Skip to main content

max / synckit

78.4 KB · 2952 lines History Blame Raw
1 {
2 "openapi": "3.1.0",
3 "info": {
4 "title": "Makenotwork API",
5 "description": "Creator marketplace API. Only public and stable endpoints are documented.",
6 "license": {
7 "name": "PolyForm Noncommercial 1.0.0"
8 },
9 "version": "0.16.7"
10 },
11 "paths": {
12 "/api/git/{owner}/{repo}/notes": {
13 "get": {
14 "tags": [
15 "Git Notes"
16 ],
17 "summary": "`GET /api/git/{owner}/{repo}/notes`: the namespaces this repository carries.",
18 "operationId": "list_namespaces",
19 "parameters": [
20 {
21 "name": "owner",
22 "in": "path",
23 "description": "Repository owner's username",
24 "required": true,
25 "schema": {
26 "type": "string"
27 }
28 },
29 {
30 "name": "repo",
31 "in": "path",
32 "description": "Repository name",
33 "required": true,
34 "schema": {
35 "type": "string"
36 }
37 }
38 ],
39 "responses": {
40 "200": {
41 "description": "Namespaces, with a note count each",
42 "content": {
43 "application/json": {
44 "schema": {
45 "$ref": "#/components/schemas/NamespacesResponse"
46 }
47 }
48 }
49 },
50 "404": {
51 "description": "No such repository, or not visible to the caller"
52 }
53 }
54 }
55 },
56 "/api/git/{owner}/{repo}/notes/search": {
57 "get": {
58 "tags": [
59 "Git Notes"
60 ],
61 "summary": "`GET /api/git/{owner}/{repo}/notes/search`: full-text search over the index.",
62 "description": "`search` is a static segment and an object id is 40 or 64 hex characters, so\nit can never be shadowed by, or shadow, a real target on `/notes/{target}`.",
63 "operationId": "search_notes",
64 "parameters": [
65 {
66 "name": "owner",
67 "in": "path",
68 "description": "Repository owner's username",
69 "required": true,
70 "schema": {
71 "type": "string"
72 }
73 },
74 {
75 "name": "repo",
76 "in": "path",
77 "description": "Repository name",
78 "required": true,
79 "schema": {
80 "type": "string"
81 }
82 },
83 {
84 "name": "q",
85 "in": "query",
86 "description": "Query: bare words, quoted phrases, `or`, `-excluded`",
87 "required": true,
88 "schema": {
89 "type": "string"
90 }
91 },
92 {
93 "name": "namespace",
94 "in": "query",
95 "description": "Restrict to one namespace",
96 "required": false,
97 "schema": {
98 "type": "string"
99 }
100 },
101 {
102 "name": "commits_only",
103 "in": "query",
104 "description": "Drop notes on blobs and trees",
105 "required": false,
106 "schema": {
107 "type": "boolean"
108 }
109 },
110 {
111 "name": "limit",
112 "in": "query",
113 "description": "Maximum hits, default 50, capped at 200",
114 "required": false,
115 "schema": {
116 "type": "integer",
117 "format": "int64"
118 }
119 }
120 ],
121 "responses": {
122 "200": {
123 "description": "Matching notes, and whether the index has seen this repository",
124 "content": {
125 "application/json": {
126 "schema": {
127 "$ref": "#/components/schemas/SearchResponse"
128 }
129 }
130 }
131 },
132 "404": {
133 "description": "No such repository, or not visible to the caller"
134 }
135 }
136 }
137 },
138 "/api/git/{owner}/{repo}/notes/{target}": {
139 "get": {
140 "tags": [
141 "Git Notes"
142 ],
143 "summary": "`GET /api/git/{owner}/{repo}/notes/{target}`: one note.",
144 "operationId": "get_note",
145 "parameters": [
146 {
147 "name": "owner",
148 "in": "path",
149 "description": "Repository owner's username",
150 "required": true,
151 "schema": {
152 "type": "string"
153 }
154 },
155 {
156 "name": "repo",
157 "in": "path",
158 "description": "Repository name",
159 "required": true,
160 "schema": {
161 "type": "string"
162 }
163 },
164 {
165 "name": "target",
166 "in": "path",
167 "description": "Full object id of the annotated object",
168 "required": true,
169 "schema": {
170 "type": "string"
171 }
172 },
173 {
174 "name": "namespace",
175 "in": "query",
176 "description": "Notes namespace, default `commits`",
177 "required": false,
178 "schema": {
179 "type": "string"
180 }
181 },
182 {
183 "name": "attribution",
184 "in": "query",
185 "description": "Include who wrote the note; costs a bounded walk of the notes ref",
186 "required": false,
187 "schema": {
188 "type": "boolean"
189 }
190 }
191 ],
192 "responses": {
193 "200": {
194 "description": "The note",
195 "content": {
196 "application/json": {
197 "schema": {
198 "$ref": "#/components/schemas/NoteResponse"
199 }
200 }
201 }
202 },
203 "404": {
204 "description": "No such repository, namespace, or note"
205 }
206 }
207 },
208 "put": {
209 "tags": [
210 "Git Notes"
211 ],
212 "summary": "`PUT /api/git/{owner}/{repo}/notes/{target}`: add or replace a note.",
213 "operationId": "put_note",
214 "parameters": [
215 {
216 "name": "owner",
217 "in": "path",
218 "description": "Repository owner's username",
219 "required": true,
220 "schema": {
221 "type": "string"
222 }
223 },
224 {
225 "name": "repo",
226 "in": "path",
227 "description": "Repository name",
228 "required": true,
229 "schema": {
230 "type": "string"
231 }
232 },
233 {
234 "name": "target",
235 "in": "path",
236 "description": "Full object id of the commit to annotate",
237 "required": true,
238 "schema": {
239 "type": "string"
240 }
241 }
242 ],
243 "requestBody": {
244 "content": {
245 "application/json": {
246 "schema": {
247 "$ref": "#/components/schemas/PutNoteRequest"
248 }
249 }
250 },
251 "required": true
252 },
253 "responses": {
254 "200": {
255 "description": "What the write did",
256 "content": {
257 "application/json": {
258 "schema": {
259 "$ref": "#/components/schemas/WriteResponse"
260 }
261 }
262 }
263 },
264 "401": {
265 "description": "No credential; writes need a push-scoped personal access token"
266 },
267 "403": {
268 "description": "A session cookie, a read-only token, or an account that cannot push here"
269 },
270 "404": {
271 "description": "No such repository, or no such commit in it"
272 },
273 "422": {
274 "description": "Reserved namespace, empty or oversized content, or sustained write contention"
275 }
276 }
277 },
278 "delete": {
279 "tags": [
280 "Git Notes"
281 ],
282 "summary": "`DELETE /api/git/{owner}/{repo}/notes/{target}`: remove a note.",
283 "operationId": "delete_note",
284 "parameters": [
285 {
286 "name": "owner",
287 "in": "path",
288 "description": "Repository owner's username",
289 "required": true,
290 "schema": {
291 "type": "string"
292 }
293 },
294 {
295 "name": "repo",
296 "in": "path",
297 "description": "Repository name",
298 "required": true,
299 "schema": {
300 "type": "string"
301 }
302 },
303 {
304 "name": "target",
305 "in": "path",
306 "description": "Full object id of the annotated commit",
307 "required": true,
308 "schema": {
309 "type": "string"
310 }
311 },
312 {
313 "name": "namespace",
314 "in": "query",
315 "description": "Notes namespace, default `commits`",
316 "required": false,
317 "schema": {
318 "type": "string"
319 }
320 }
321 ],
322 "responses": {
323 "204": {
324 "description": "The note is gone, whether or not it was there"
325 },
326 "401": {
327 "description": "No credential; writes need a push-scoped personal access token"
328 },
329 "403": {
330 "description": "A session cookie, a read-only token, or an account that cannot push here"
331 },
332 "404": {
333 "description": "No such repository, or no such commit in it"
334 }
335 }
336 }
337 },
338 "/api/v1/items/{item_id}/license.txt": {
339 "get": {
340 "tags": [
341 "License Keys"
342 ],
343 "summary": "Serve rendered license text for an item as plain text.",
344 "operationId": "license_text",
345 "parameters": [
346 {
347 "name": "item_id",
348 "in": "path",
349 "description": "The item ID",
350 "required": true,
351 "schema": {
352 "type": "string"
353 }
354 }
355 ],
356 "responses": {
357 "200": {
358 "description": "License text",
359 "content": {
360 "text/plain": {}
361 }
362 },
363 "404": {
364 "description": "Item not found or no license configured"
365 }
366 }
367 }
368 },
369 "/api/v1/keys/deactivate": {
370 "post": {
371 "tags": [
372 "License Keys"
373 ],
374 "summary": "Release an activation slot (user uninstalls).",
375 "operationId": "deactivate_key",
376 "requestBody": {
377 "content": {
378 "application/json": {
379 "schema": {
380 "$ref": "#/components/schemas/DeactivateKeyRequest"
381 }
382 }
383 },
384 "required": true
385 },
386 "responses": {
387 "200": {
388 "description": "Deactivation result",
389 "content": {
390 "application/json": {
391 "schema": {
392 "$ref": "#/components/schemas/DeactivateKeyResponse"
393 }
394 }
395 }
396 }
397 }
398 }
399 },
400 "/api/v1/keys/status": {
401 "post": {
402 "tags": [
403 "License Keys"
404 ],
405 "summary": "Quick validity check without activating, key in the POST body.",
406 "description": "Prefer this over the GET `/keys/{key_code}/status` form: the key code is a\npurchase-proof secret, and putting it in the URL path leaks it into access\nand proxy logs. This matches the other license endpoints (validate,\ndeactivate, verify), which all carry the key in the body.",
407 "operationId": "key_status_post",
408 "requestBody": {
409 "content": {
410 "application/json": {
411 "schema": {
412 "$ref": "#/components/schemas/KeyStatusRequest"
413 }
414 }
415 },
416 "required": true
417 },
418 "responses": {
419 "200": {
420 "description": "Key status",
421 "content": {
422 "application/json": {
423 "schema": {
424 "$ref": "#/components/schemas/KeyStatusResponse"
425 }
426 }
427 }
428 }
429 }
430 }
431 },
432 "/api/v1/keys/validate": {
433 "post": {
434 "tags": [
435 "License Keys"
436 ],
437 "summary": "Validate a license key and optionally activate it on a machine.",
438 "operationId": "validate_key",
439 "requestBody": {
440 "content": {
441 "application/json": {
442 "schema": {
443 "$ref": "#/components/schemas/ValidateKeyRequest"
444 }
445 }
446 },
447 "required": true
448 },
449 "responses": {
450 "200": {
451 "description": "Validation result",
452 "content": {
453 "application/json": {
454 "schema": {
455 "$ref": "#/components/schemas/ValidateKeyResponse"
456 }
457 }
458 }
459 }
460 }
461 }
462 },
463 "/api/v1/keys/{key_code}/status": {
464 "get": {
465 "tags": [
466 "License Keys"
467 ],
468 "summary": "Quick validity check without activating.",
469 "description": "DEPRECATED: the key code rides in the URL path, which leaks this\npurchase-proof secret into access/proxy logs. Use `POST /api/v1/keys/status`\n(key in the body) instead. Kept for backward compatibility with SDK\nconsumers that predate the POST form.",
470 "operationId": "key_status",
471 "parameters": [
472 {
473 "name": "key_code",
474 "in": "path",
475 "description": "The license key code",
476 "required": true,
477 "schema": {
478 "type": "string"
479 }
480 }
481 ],
482 "responses": {
483 "200": {
484 "description": "Key status (DEPRECATED. Prefer POST /api/v1/keys/status)",
485 "content": {
486 "application/json": {
487 "schema": {
488 "$ref": "#/components/schemas/KeyStatusResponse"
489 }
490 }
491 }
492 }
493 }
494 }
495 },
496 "/api/v1/license/deactivate": {
497 "post": {
498 "tags": [
499 "License Keys"
500 ],
501 "summary": "Deactivate a license on a specific machine (free up a slot).",
502 "operationId": "license_deactivate",
503 "requestBody": {
504 "content": {
505 "application/json": {
506 "schema": {
507 "$ref": "#/components/schemas/LicenseDeactivateRequest"
508 }
509 }
510 },
511 "required": true
512 },
513 "responses": {
514 "200": {
515 "description": "Deactivation result",
516 "content": {
517 "application/json": {
518 "schema": {
519 "$ref": "#/components/schemas/DeactivateKeyResponse"
520 }
521 }
522 }
523 }
524 }
525 }
526 },
527 "/api/v1/license/verify": {
528 "post": {
529 "tags": [
530 "License Keys"
531 ],
532 "summary": "Verify a license key and bind it to a machine fingerprint.",
533 "description": "If the project has `license_verification_enabled`, validates the key,\nchecks/creates an activation (using machine_fingerprint as machine_id),\nand returns a signed JWT for offline verification (valid 7 days).",
534 "operationId": "license_verify",
535 "requestBody": {
536 "content": {
537 "application/json": {
538 "schema": {
539 "$ref": "#/components/schemas/LicenseVerifyRequest"
540 }
541 }
542 },
543 "required": true
544 },
545 "responses": {
546 "200": {
547 "description": "Verification result with optional offline JWT",
548 "content": {
549 "application/json": {
550 "schema": {
551 "$ref": "#/components/schemas/LicenseVerifyResponse"
552 }
553 }
554 }
555 }
556 }
557 }
558 },
559 "/api/v1/sync/account": {
560 "get": {
561 "tags": [
562 "SyncKit"
563 ],
564 "summary": "Return the authenticated user's email and username, for the app to display\n\"logged in as ...\" in its sync UI.",
565 "operationId": "sync_account",
566 "responses": {
567 "200": {
568 "description": "Account info",
569 "content": {
570 "application/json": {
571 "schema": {
572 "$ref": "#/components/schemas/SyncAccountResponse"
573 }
574 }
575 }
576 }
577 },
578 "security": [
579 {
580 "bearer": []
581 }
582 ]
583 }
584 },
585 "/api/v1/sync/app/pricing": {
586 "post": {
587 "tags": [
588 "SyncKit"
589 ],
590 "summary": "Return the pricing-formula constants for an app. The client uses these to\nquote a price locally as the user adjusts the cap slider; the same formula\nis enforced server-side at checkout so the client number is only advisory.",
591 "operationId": "get_app_pricing",
592 "requestBody": {
593 "content": {
594 "application/json": {
595 "schema": {
596 "$ref": "#/components/schemas/AppPricingRequest"
597 }
598 }
599 },
600 "required": true
601 },
602 "responses": {
603 "200": {
604 "description": "Pricing formula",
605 "content": {
606 "application/json": {
607 "schema": {
608 "$ref": "#/components/schemas/AppPricingResponse"
609 }
610 }
611 }
612 }
613 }
614 }
615 },
616 "/api/v1/sync/auth": {
617 "post": {
618 "tags": [
619 "SyncKit"
620 ],
621 "summary": "Authenticate a user and return a JWT for subsequent sync API calls.",
622 "description": "Verifies the app API key, then validates user email/password credentials.\nReturns a short-lived JWT containing the user ID and app ID, which the\nclient SDK includes as a Bearer token on all other sync endpoints.",
623 "operationId": "sync_auth",
624 "requestBody": {
625 "content": {
626 "application/json": {
627 "schema": {
628 "$ref": "#/components/schemas/SyncAuthRequest"
629 }
630 }
631 },
632 "required": true
633 },
634 "responses": {
635 "200": {
636 "description": "JWT token for sync API access",
637 "content": {
638 "application/json": {
639 "schema": {
640 "$ref": "#/components/schemas/SyncAuthResponse"
641 }
642 }
643 }
644 },
645 "401": {
646 "description": "Invalid credentials or API key"
647 }
648 }
649 }
650 },
651 "/api/v1/sync/blobs/confirm": {
652 "post": {
653 "tags": [
654 "SyncKit"
655 ],
656 "summary": "Confirm that a blob upload to S3 completed successfully.",
657 "description": "Verifies the object exists in S3, then records it in the database.\nIdempotent: returns success without creating a duplicate.\n\nContent-addressing trust model: the blob `hash` is treated as a\ncontent-address LABEL, confirm reads the authoritative `object_size` from S3\nbut does not re-hash the bytes to prove they match `hash`. The blast radius\nis per-user only: the key is `{app_id}/{user_id}/{hash}` and storage is\n`UNIQUE(app_id, user_id, hash)`, so a client that stores mismatched bytes can\npoison only its OWN dedup namespace, no cross-user effect, no data exposure.\n\nThis note used to say the A+ fix was binding `x-amz-checksum-sha256` into the\npresigned PUT so S3 rejects a mismatched upload at write time. That reasoning\ndoes not hold for these blobs, and the correction is worth keeping: the stored\nobject is E2E *ciphertext* sealed with random per-chunk nonces, while `hash`\nis the SHA-256 of the *plaintext*. The server never sees plaintext, so it\ncannot derive the expected ciphertext digest at presign time, any checksum it\nbinds has to come from the client, i.e. the party whose honesty was in\nquestion. Checksum binding (which the multipart path now does per part) buys\ntransport integrity, not content-address enforcement.\n\nWhat actually binds the bytes to the address is the AEAD: each chunk is sealed\nwith `(hash, chunk_index, chunk_count)` as associated data, so ciphertext that\nopens under `hash` is cryptographically tied to it, and the client re-hashes\nthe plaintext after decrypting. A client storing mismatched bytes breaks only\nits own blob. Server-side re-hashing would cost a full object download per\nconfirm to defend a client against itself, which is why it is not done.",
658 "operationId": "blob_confirm_upload",
659 "requestBody": {
660 "content": {
661 "application/json": {
662 "schema": {
663 "$ref": "#/components/schemas/BlobConfirmRequest"
664 }
665 }
666 },
667 "required": true
668 },
669 "responses": {
670 "204": {
671 "description": "Upload confirmed"
672 }
673 },
674 "security": [
675 {
676 "bearer": []
677 }
678 ]
679 }
680 },
681 "/api/v1/sync/blobs/download": {
682 "post": {
683 "tags": [
684 "SyncKit"
685 ],
686 "summary": "Request a pre-signed S3 download URL for a blob by hash.",
687 "operationId": "blob_download_url",
688 "requestBody": {
689 "content": {
690 "application/json": {
691 "schema": {
692 "$ref": "#/components/schemas/BlobDownloadUrlRequest"
693 }
694 }
695 },
696 "required": true
697 },
698 "responses": {
699 "200": {
700 "description": "Pre-signed download URL",
701 "content": {
702 "application/json": {
703 "schema": {
704 "$ref": "#/components/schemas/BlobDownloadUrlResponse"
705 }
706 }
707 }
708 },
709 "404": {
710 "description": "Blob not found"
711 }
712 },
713 "security": [
714 {
715 "bearer": []
716 }
717 ]
718 }
719 },
720 "/api/v1/sync/blobs/multipart/abort": {
721 "post": {
722 "tags": [
723 "SyncKit"
724 ],
725 "summary": "Release the parts of an abandoned session (client cancel).",
726 "description": "Incomplete multipart uploads bill for their parts until aborted, so a client\nthat cleans up on cancel is the cheapest fix; the orphan reaper is the\nbackstop for clients that vanish.",
727 "operationId": "blob_multipart_abort",
728 "requestBody": {
729 "content": {
730 "application/json": {
731 "schema": {
732 "$ref": "#/components/schemas/BlobMultipartAbortRequest"
733 }
734 }
735 },
736 "required": true
737 },
738 "responses": {
739 "204": {
740 "description": "Session aborted"
741 }
742 },
743 "security": [
744 {
745 "bearer": []
746 }
747 ]
748 }
749 },
750 "/api/v1/sync/blobs/multipart/complete": {
751 "post": {
752 "tags": [
753 "SyncKit"
754 ],
755 "summary": "Assemble the uploaded parts into the blob object.",
756 "description": "Transport only: the client then calls `/blobs/confirm`, which reads the real\nobject size from S3 and applies every quota and billing rule.",
757 "operationId": "blob_multipart_complete",
758 "requestBody": {
759 "content": {
760 "application/json": {
761 "schema": {
762 "$ref": "#/components/schemas/BlobMultipartCompleteRequest"
763 }
764 }
765 },
766 "required": true
767 },
768 "responses": {
769 "204": {
770 "description": "Parts assembled"
771 }
772 },
773 "security": [
774 {
775 "bearer": []
776 }
777 ]
778 }
779 },
780 "/api/v1/sync/blobs/multipart/parts": {
781 "post": {
782 "tags": [
783 "SyncKit"
784 ],
785 "summary": "Mint a bounded window of presigned `UploadPart` URLs, each carrying its exact\nsigned `Content-Length`, the same defense-in-depth the one-shot presign\napplies.",
786 "operationId": "blob_multipart_parts",
787 "requestBody": {
788 "content": {
789 "application/json": {
790 "schema": {
791 "$ref": "#/components/schemas/BlobMultipartPartsRequest"
792 }
793 }
794 },
795 "required": true
796 },
797 "responses": {
798 "200": {
799 "description": "Presigned part URLs",
800 "content": {
801 "application/json": {
802 "schema": {
803 "$ref": "#/components/schemas/BlobMultipartPartsResponse"
804 }
805 }
806 }
807 }
808 },
809 "security": [
810 {
811 "bearer": []
812 }
813 ]
814 }
815 },
816 "/api/v1/sync/blobs/multipart/start": {
817 "post": {
818 "tags": [
819 "SyncKit"
820 ],
821 "summary": "Open a multipart upload session for a large blob.",
822 "description": "`size_bytes` is the ciphertext length, which the client derives from the\nplaintext length alone (`blob_encrypted_len`) before sealing anything. The\npart geometry is pure arithmetic over it, so both sides compute identical\nboundaries without a round trip.",
823 "operationId": "blob_multipart_start",
824 "requestBody": {
825 "content": {
826 "application/json": {
827 "schema": {
828 "$ref": "#/components/schemas/BlobMultipartStartRequest"
829 }
830 }
831 },
832 "required": true
833 },
834 "responses": {
835 "200": {
836 "description": "Multipart session opened",
837 "content": {
838 "application/json": {
839 "schema": {
840 "$ref": "#/components/schemas/BlobMultipartStartResponse"
841 }
842 }
843 }
844 }
845 },
846 "security": [
847 {
848 "bearer": []
849 }
850 ]
851 }
852 },
853 "/api/v1/sync/blobs/upload": {
854 "post": {
855 "tags": [
856 "SyncKit"
857 ],
858 "summary": "Request a pre-signed S3 upload URL for a blob.",
859 "description": "Content-addressed by hash: if a blob with the same hash already exists\nfor this user/app, returns `already_exists: true` and an empty URL,\nskipping the upload.",
860 "operationId": "blob_upload_url",
861 "requestBody": {
862 "content": {
863 "application/json": {
864 "schema": {
865 "$ref": "#/components/schemas/BlobUploadUrlRequest"
866 }
867 }
868 },
869 "required": true
870 },
871 "responses": {
872 "200": {
873 "description": "Pre-signed upload URL",
874 "content": {
875 "application/json": {
876 "schema": {
877 "$ref": "#/components/schemas/BlobUploadUrlResponse"
878 }
879 }
880 }
881 }
882 },
883 "security": [
884 {
885 "bearer": []
886 }
887 ]
888 }
889 },
890 "/api/v1/sync/devices": {
891 "get": {
892 "tags": [
893 "SyncKit"
894 ],
895 "summary": "List all devices registered for the authenticated user and app.",
896 "operationId": "list_devices",
897 "responses": {
898 "200": {
899 "description": "List of devices",
900 "content": {
901 "application/json": {
902 "schema": {
903 "type": "array",
904 "items": {
905 "$ref": "#/components/schemas/SyncDeviceResponse"
906 }
907 }
908 }
909 }
910 }
911 },
912 "security": [
913 {
914 "bearer": []
915 }
916 ]
917 },
918 "post": {
919 "tags": [
920 "SyncKit"
921 ],
922 "summary": "Register a new sync device (or update an existing one by name).",
923 "operationId": "register_device",
924 "requestBody": {
925 "content": {
926 "application/json": {
927 "schema": {
928 "$ref": "#/components/schemas/RegisterDeviceRequest"
929 }
930 }
931 },
932 "required": true
933 },
934 "responses": {
935 "200": {
936 "description": "Registered device",
937 "content": {
938 "application/json": {
939 "schema": {
940 "$ref": "#/components/schemas/SyncDeviceResponse"
941 }
942 }
943 }
944 }
945 },
946 "security": [
947 {
948 "bearer": []
949 }
950 ]
951 }
952 },
953 "/api/v1/sync/devices/{id}": {
954 "delete": {
955 "tags": [
956 "SyncKit"
957 ],
958 "summary": "Remove a registered device.",
959 "operationId": "delete_device",
960 "parameters": [
961 {
962 "name": "id",
963 "in": "path",
964 "description": "Device ID",
965 "required": true,
966 "schema": {
967 "type": "string"
968 }
969 }
970 ],
971 "responses": {
972 "204": {
973 "description": "Device deleted"
974 },
975 "404": {
976 "description": "Device not found"
977 }
978 },
979 "security": [
980 {
981 "bearer": []
982 }
983 ]
984 }
985 },
986 "/api/v1/sync/keys": {
987 "get": {
988 "tags": [
989 "SyncKit"
990 ],
991 "summary": "Retrieve the user's encrypted master key envelope.",
992 "operationId": "get_sync_key",
993 "responses": {
994 "200": {
995 "description": "Encrypted key envelope",
996 "content": {
997 "application/json": {
998 "schema": {
999 "$ref": "#/components/schemas/GetKeyResponse"
1000 }
1001 }
1002 }
1003 },
1004 "404": {
1005 "description": "No key stored"
1006 }
1007 },
1008 "security": [
1009 {
1010 "bearer": []
1011 }
1012 ]
1013 },
1014 "put": {
1015 "tags": [
1016 "SyncKit"
1017 ],
1018 "summary": "Store or update the user's encrypted master key envelope.",
1019 "operationId": "put_sync_key",
1020 "requestBody": {
1021 "content": {
1022 "application/json": {
1023 "schema": {
1024 "$ref": "#/components/schemas/PutKeyRequest"
1025 }
1026 }
1027 },
1028 "required": true
1029 },
1030 "responses": {
1031 "204": {
1032 "description": "Key stored"
1033 },
1034 "409": {
1035 "description": "Version mismatch"
1036 }
1037 },
1038 "security": [
1039 {
1040 "bearer": []
1041 }
1042 ]
1043 }
1044 },
1045 "/api/v1/sync/keys/rotate": {
1046 "post": {
1047 "tags": [
1048 "SyncKit"
1049 ],
1050 "summary": "Begin a key rotation.",
1051 "operationId": "begin_rotation",
1052 "requestBody": {
1053 "content": {
1054 "application/json": {
1055 "schema": {
1056 "$ref": "#/components/schemas/BeginRotationRequest"
1057 }
1058 }
1059 },
1060 "required": true
1061 },
1062 "responses": {
1063 "200": {
1064 "description": "Rotation started or resumed",
1065 "content": {
1066 "application/json": {
1067 "schema": {
1068 "$ref": "#/components/schemas/BeginRotationResponse"
1069 }
1070 }
1071 }
1072 },
1073 "409": {
1074 "description": "Version mismatch or rotation in progress"
1075 }
1076 },
1077 "security": [
1078 {
1079 "bearer": []
1080 }
1081 ]
1082 },
1083 "delete": {
1084 "tags": [
1085 "SyncKit"
1086 ],
1087 "summary": "Cancel a stale rotation (>24h without activity).",
1088 "operationId": "cancel_rotation",
1089 "responses": {
1090 "204": {
1091 "description": "Stale rotation cancelled"
1092 },
1093 "404": {
1094 "description": "No stale rotation found"
1095 }
1096 },
1097 "security": [
1098 {
1099 "bearer": []
1100 }
1101 ]
1102 }
1103 },
1104 "/api/v1/sync/keys/rotate/batch": {
1105 "post": {
1106 "tags": [
1107 "SyncKit"
1108 ],
1109 "summary": "Submit a batch of re-encrypted entries during rotation.",
1110 "operationId": "rotation_batch",
1111 "requestBody": {
1112 "content": {
1113 "application/json": {
1114 "schema": {
1115 "$ref": "#/components/schemas/RotationBatchRequest"
1116 }
1117 }
1118 },
1119 "required": true
1120 },
1121 "responses": {
1122 "200": {
1123 "description": "Batch processed",
1124 "content": {
1125 "application/json": {
1126 "schema": {
1127 "$ref": "#/components/schemas/RotationBatchResponse"
1128 }
1129 }
1130 }
1131 }
1132 },
1133 "security": [
1134 {
1135 "bearer": []
1136 }
1137 ]
1138 }
1139 },
1140 "/api/v1/sync/keys/rotate/complete": {
1141 "post": {
1142 "tags": [
1143 "SyncKit"
1144 ],
1145 "summary": "Complete a key rotation.",
1146 "operationId": "complete_rotation",
1147 "requestBody": {
1148 "content": {
1149 "application/json": {
1150 "schema": {
1151 "$ref": "#/components/schemas/CompleteRotationRequest"
1152 }
1153 }
1154 },
1155 "required": true
1156 },
1157 "responses": {
1158 "204": {
1159 "description": "Rotation completed"
1160 },
1161 "409": {
1162 "description": "Entries still need re-encryption"
1163 }
1164 },
1165 "security": [
1166 {
1167 "bearer": []
1168 }
1169 ]
1170 }
1171 },
1172 "/api/v1/sync/keys/rotate/entries": {
1173 "post": {
1174 "tags": [
1175 "SyncKit"
1176 ],
1177 "summary": "Pull entries that need re-encryption during a rotation.",
1178 "operationId": "rotation_entries",
1179 "requestBody": {
1180 "content": {
1181 "application/json": {
1182 "schema": {
1183 "$ref": "#/components/schemas/RotationEntriesRequest"
1184 }
1185 }
1186 },
1187 "required": true
1188 },
1189 "responses": {
1190 "200": {
1191 "description": "Entries needing re-encryption",
1192 "content": {
1193 "application/json": {
1194 "schema": {
1195 "$ref": "#/components/schemas/RotationEntriesResponse"
1196 }
1197 }
1198 }
1199 }
1200 },
1201 "security": [
1202 {
1203 "bearer": []
1204 }
1205 ]
1206 }
1207 },
1208 "/api/v1/sync/pull": {
1209 "post": {
1210 "tags": [
1211 "SyncKit"
1212 ],
1213 "summary": "Pull changelog entries after a given cursor.",
1214 "operationId": "sync_pull",
1215 "requestBody": {
1216 "content": {
1217 "application/json": {
1218 "schema": {
1219 "$ref": "#/components/schemas/PullRequest"
1220 }
1221 }
1222 },
1223 "required": true
1224 },
1225 "responses": {
1226 "200": {
1227 "description": "Changes since cursor",
1228 "content": {
1229 "application/json": {
1230 "schema": {
1231 "$ref": "#/components/schemas/PullResponse"
1232 }
1233 }
1234 }
1235 }
1236 },
1237 "security": [
1238 {
1239 "bearer": []
1240 }
1241 ]
1242 }
1243 },
1244 "/api/v1/sync/push": {
1245 "post": {
1246 "tags": [
1247 "SyncKit"
1248 ],
1249 "summary": "Push encrypted changelog entries from a device.",
1250 "operationId": "sync_push",
1251 "requestBody": {
1252 "content": {
1253 "application/json": {
1254 "schema": {
1255 "$ref": "#/components/schemas/PushRequest"
1256 }
1257 }
1258 },
1259 "required": true
1260 },
1261 "responses": {
1262 "200": {
1263 "description": "New cursor position",
1264 "content": {
1265 "application/json": {
1266 "schema": {
1267 "$ref": "#/components/schemas/PushResponse"
1268 }
1269 }
1270 }
1271 }
1272 },
1273 "security": [
1274 {
1275 "bearer": []
1276 }
1277 ]
1278 }
1279 },
1280 "/api/v1/sync/status": {
1281 "get": {
1282 "tags": [
1283 "SyncKit"
1284 ],
1285 "summary": "Return sync metadata for the authenticated user and app.",
1286 "operationId": "sync_status",
1287 "responses": {
1288 "200": {
1289 "description": "Sync status",
1290 "content": {
1291 "application/json": {
1292 "schema": {
1293 "$ref": "#/components/schemas/SyncStatusResponse"
1294 }
1295 }
1296 }
1297 }
1298 },
1299 "security": [
1300 {
1301 "bearer": []
1302 }
1303 ]
1304 }
1305 },
1306 "/api/v1/sync/subscription": {
1307 "get": {
1308 "tags": [
1309 "SyncKit"
1310 ],
1311 "summary": "Return the authenticated user's subscription status for this app.\nReturns `active: false` and `None` fields when the user has no subscription\n(rather than 404) so clients can render a \"subscribe\" CTA uniformly.",
1312 "operationId": "sync_subscription_status",
1313 "responses": {
1314 "200": {
1315 "description": "Subscription status",
1316 "content": {
1317 "application/json": {
1318 "schema": {
1319 "$ref": "#/components/schemas/SyncSubscriptionStatusResponse"
1320 }
1321 }
1322 }
1323 }
1324 },
1325 "security": [
1326 {
1327 "bearer": []
1328 }
1329 ]
1330 }
1331 },
1332 "/api/v1/sync/subscription/checkout": {
1333 "post": {
1334 "tags": [
1335 "SyncKit"
1336 ],
1337 "summary": "Create a Stripe Checkout Session for subscribing this user to the app's\ncloud sync at their chosen storage cap. The `app_sync_subscriptions` row\nis written by the Stripe webhook on `checkout.session.completed`.",
1338 "operationId": "create_subscription_checkout",
1339 "requestBody": {
1340 "content": {
1341 "application/json": {
1342 "schema": {
1343 "$ref": "#/components/schemas/SyncSubscribeRequest"
1344 }
1345 }
1346 },
1347 "required": true
1348 },
1349 "responses": {
1350 "200": {
1351 "description": "Checkout URL",
1352 "content": {
1353 "application/json": {
1354 "schema": {
1355 "$ref": "#/components/schemas/SyncCheckoutResponse"
1356 }
1357 }
1358 }
1359 }
1360 },
1361 "security": [
1362 {
1363 "bearer": []
1364 }
1365 ]
1366 }
1367 },
1368 "/api/v1/sync/subscription/quote": {
1369 "post": {
1370 "tags": [
1371 "SyncKit"
1372 ],
1373 "summary": "Quote the price for a (cap, interval) pair. Authenticated so clients\ncannot scrape pricing without an account, but otherwise pure: the result\nonly depends on the formula constants returned by `app/pricing`.",
1374 "operationId": "quote_subscription_price",
1375 "requestBody": {
1376 "content": {
1377 "application/json": {
1378 "schema": {
1379 "$ref": "#/components/schemas/SyncQuoteRequest"
1380 }
1381 }
1382 },
1383 "required": true
1384 },
1385 "responses": {
1386 "200": {
1387 "description": "Quoted price",
1388 "content": {
1389 "application/json": {
1390 "schema": {
1391 "$ref": "#/components/schemas/SyncQuoteResponse"
1392 }
1393 }
1394 }
1395 }
1396 },
1397 "security": [
1398 {
1399 "bearer": []
1400 }
1401 ]
1402 }
1403 },
1404 "/api/v1/sync/subscription/storage-cap": {
1405 "post": {
1406 "tags": [
1407 "SyncKit"
1408 ],
1409 "summary": "Queue a storage-cap change to take effect at the next billing cycle.\nHolding the change for the cycle boundary avoids mid-cycle proration\nsurprises and keeps the user in control of when their bill changes.",
1410 "description": "Updates Stripe first (re-price the subscription item with\n`proration_behavior=None`), then records the pending cap in the DB. The\nrenewal webhook promotes the pending cap to active when Stripe rolls the\nperiod, and because the Stripe price is already updated, the new invoice\nis at the new price.",
1411 "operationId": "queue_storage_cap_change",
1412 "requestBody": {
1413 "content": {
1414 "application/json": {
1415 "schema": {
1416 "$ref": "#/components/schemas/SyncCapChangeRequest"
1417 }
1418 }
1419 },
1420 "required": true
1421 },
1422 "responses": {
1423 "200": {
1424 "description": "Pending cap recorded",
1425 "content": {
1426 "application/json": {
1427 "schema": {
1428 "$ref": "#/components/schemas/SyncSubscriptionStatusResponse"
1429 }
1430 }
1431 }
1432 }
1433 },
1434 "security": [
1435 {
1436 "bearer": []
1437 }
1438 ]
1439 }
1440 },
1441 "/api/v1/sync/validate-app": {
1442 "post": {
1443 "tags": [
1444 "SyncKit"
1445 ],
1446 "summary": "Validate an API key without authentication. Returns the app name on success.",
1447 "description": "API key is sent in the JSON body (not query string) to avoid log exposure.",
1448 "operationId": "validate_app",
1449 "requestBody": {
1450 "content": {
1451 "application/json": {
1452 "schema": {
1453 "$ref": "#/components/schemas/ValidateAppQuery"
1454 }
1455 }
1456 },
1457 "required": true
1458 },
1459 "responses": {
1460 "200": {
1461 "description": "App name",
1462 "content": {
1463 "application/json": {
1464 "schema": {
1465 "$ref": "#/components/schemas/ValidateAppResponse"
1466 }
1467 }
1468 }
1469 },
1470 "401": {
1471 "description": "Invalid API key"
1472 }
1473 }
1474 }
1475 }
1476 },
1477 "components": {
1478 "schemas": {
1479 "AppPricingRequest": {
1480 "type": "object",
1481 "description": "Request body for `POST /api/v1/sync/app/pricing`. Identifies the app by\nits public API key; no JWT required so the UI can quote pricing pre-login.",
1482 "required": [
1483 "api_key"
1484 ],
1485 "properties": {
1486 "api_key": {
1487 "type": "string"
1488 }
1489 }
1490 },
1491 "AppPricingResponse": {
1492 "type": "object",
1493 "description": "Pricing formula constants the client uses to quote a price locally as the\nuser drags a cap slider. The same formula is enforced server-side at\ncheckout, clients are not trusted to compute the final price.",
1494 "required": [
1495 "app_name",
1496 "min_charge_cents",
1497 "per_gb_tenths_of_cent_per_month",
1498 "annual_multiplier",
1499 "min_cap_bytes",
1500 "max_cap_bytes"
1501 ],
1502 "properties": {
1503 "annual_multiplier": {
1504 "type": "integer",
1505 "format": "int64",
1506 "description": "Annual is monthly × this value."
1507 },
1508 "app_name": {
1509 "type": "string"
1510 },
1511 "max_cap_bytes": {
1512 "type": "integer",
1513 "format": "int64"
1514 },
1515 "min_cap_bytes": {
1516 "type": "integer",
1517 "format": "int64"
1518 },
1519 "min_charge_cents": {
1520 "type": "integer",
1521 "format": "int64",
1522 "description": "Floor charge in cents (monthly or annual, same floor applies to both)."
1523 },
1524 "per_gb_tenths_of_cent_per_month": {
1525 "type": "integer",
1526 "format": "int64",
1527 "description": "Per-GiB monthly storage rate, in tenths of a cent."
1528 }
1529 }
1530 },
1531 "BeginRotationRequest": {
1532 "type": "object",
1533 "required": [
1534 "device_id",
1535 "new_encrypted_key",
1536 "expected_key_version"
1537 ],
1538 "properties": {
1539 "device_id": {
1540 "type": "string"
1541 },
1542 "expected_key_version": {
1543 "type": "integer",
1544 "format": "int32"
1545 },
1546 "new_encrypted_key": {
1547 "type": "string"
1548 }
1549 }
1550 },
1551 "BeginRotationResponse": {
1552 "type": "object",
1553 "required": [
1554 "rotation_id",
1555 "target_seq",
1556 "new_key_id"
1557 ],
1558 "properties": {
1559 "new_key_id": {
1560 "type": "integer",
1561 "format": "int32"
1562 },
1563 "rotation_id": {
1564 "type": "string",
1565 "format": "uuid"
1566 },
1567 "target_seq": {
1568 "type": "integer",
1569 "format": "int64"
1570 }
1571 }
1572 },
1573 "BlobConfirmRequest": {
1574 "type": "object",
1575 "required": [
1576 "hash"
1577 ],
1578 "properties": {
1579 "hash": {
1580 "type": "string"
1581 }
1582 }
1583 },
1584 "BlobDownloadUrlRequest": {
1585 "type": "object",
1586 "required": [
1587 "hash"
1588 ],
1589 "properties": {
1590 "hash": {
1591 "type": "string"
1592 }
1593 }
1594 },
1595 "BlobDownloadUrlResponse": {
1596 "type": "object",
1597 "required": [
1598 "download_url"
1599 ],
1600 "properties": {
1601 "download_url": {
1602 "type": "string"
1603 }
1604 }
1605 },
1606 "BlobMultipartAbortRequest": {
1607 "type": "object",
1608 "required": [
1609 "hash",
1610 "upload_id"
1611 ],
1612 "properties": {
1613 "hash": {
1614 "type": "string"
1615 },
1616 "upload_id": {
1617 "type": "string"
1618 }
1619 }
1620 },
1621 "BlobMultipartCompleteRequest": {
1622 "type": "object",
1623 "required": [
1624 "hash",
1625 "upload_id",
1626 "parts"
1627 ],
1628 "properties": {
1629 "hash": {
1630 "type": "string"
1631 },
1632 "parts": {
1633 "type": "array",
1634 "items": {
1635 "$ref": "#/components/schemas/BlobMultipartCompletedPart"
1636 }
1637 },
1638 "upload_id": {
1639 "type": "string"
1640 }
1641 }
1642 },
1643 "BlobMultipartCompletedPart": {
1644 "type": "object",
1645 "required": [
1646 "part_number",
1647 "etag"
1648 ],
1649 "properties": {
1650 "etag": {
1651 "type": "string"
1652 },
1653 "part_number": {
1654 "type": "integer",
1655 "format": "int32"
1656 }
1657 }
1658 },
1659 "BlobMultipartPartUrl": {
1660 "type": "object",
1661 "required": [
1662 "part_number",
1663 "content_length",
1664 "url"
1665 ],
1666 "properties": {
1667 "content_length": {
1668 "type": "integer",
1669 "format": "int64",
1670 "minimum": 0
1671 },
1672 "part_number": {
1673 "type": "integer",
1674 "format": "int32"
1675 },
1676 "url": {
1677 "type": "string"
1678 }
1679 }
1680 },
1681 "BlobMultipartPartsRequest": {
1682 "type": "object",
1683 "required": [
1684 "hash",
1685 "upload_id",
1686 "size_bytes",
1687 "first_part",
1688 "count"
1689 ],
1690 "properties": {
1691 "checksums": {
1692 "type": [
1693 "array",
1694 "null"
1695 ],
1696 "items": {
1697 "type": "string"
1698 },
1699 "description": "SHA-256 of each requested part's bytes, base64 of the raw digest,\npositionally aligned with `first_part..first_part + count`. Bound into\nthe presigned URL so S3 rehashes the part and rejects a mismatch at write\ntime. Optional for now, since a client can only supply a checksum for a\npart it has already built, which in practice means asking for one part\nat a time. When present the length must equal `count`."
1700 },
1701 "count": {
1702 "type": "integer",
1703 "format": "int32",
1704 "minimum": 0
1705 },
1706 "first_part": {
1707 "type": "integer",
1708 "format": "int32",
1709 "minimum": 0
1710 },
1711 "hash": {
1712 "type": "string"
1713 },
1714 "size_bytes": {
1715 "type": "integer",
1716 "format": "int64",
1717 "description": "Must match the `size_bytes` passed to `start`, the plan is deterministic\nin it, and a different value would sign the wrong `Content-Length`s."
1718 },
1719 "upload_id": {
1720 "type": "string"
1721 }
1722 }
1723 },
1724 "BlobMultipartPartsResponse": {
1725 "type": "object",
1726 "required": [
1727 "parts",
1728 "expires_in"
1729 ],
1730 "properties": {
1731 "expires_in": {
1732 "type": "integer",
1733 "format": "int64",
1734 "minimum": 0
1735 },
1736 "parts": {
1737 "type": "array",
1738 "items": {
1739 "$ref": "#/components/schemas/BlobMultipartPartUrl"
1740 }
1741 }
1742 }
1743 },
1744 "BlobMultipartStartRequest": {
1745 "type": "object",
1746 "description": "Open a multipart blob session. `size_bytes` is the *ciphertext* length,\nwhich the client knows before sealing anything (`blob_encrypted_len`), so\nboth sides derive the same part geometry from it without a round trip.",
1747 "required": [
1748 "hash",
1749 "size_bytes"
1750 ],
1751 "properties": {
1752 "hash": {
1753 "type": "string"
1754 },
1755 "size_bytes": {
1756 "type": "integer",
1757 "format": "int64"
1758 }
1759 }
1760 },
1761 "BlobMultipartStartResponse": {
1762 "type": "object",
1763 "required": [
1764 "upload_id",
1765 "part_size",
1766 "part_count",
1767 "already_exists"
1768 ],
1769 "properties": {
1770 "already_exists": {
1771 "type": "boolean",
1772 "description": "Same dedup short-circuit as the one-shot upload: when true no session\nwas opened and the other fields are empty."
1773 },
1774 "part_count": {
1775 "type": "integer",
1776 "format": "int32",
1777 "minimum": 0
1778 },
1779 "part_size": {
1780 "type": "integer",
1781 "minimum": 0
1782 },
1783 "upload_id": {
1784 "type": "string"
1785 }
1786 }
1787 },
1788 "BlobUploadUrlRequest": {
1789 "type": "object",
1790 "required": [
1791 "hash",
1792 "size_bytes"
1793 ],
1794 "properties": {
1795 "hash": {
1796 "type": "string"
1797 },
1798 "size_bytes": {
1799 "type": "integer",
1800 "format": "int64"
1801 }
1802 }
1803 },
1804 "BlobUploadUrlResponse": {
1805 "type": "object",
1806 "required": [
1807 "upload_url",
1808 "already_exists"
1809 ],
1810 "properties": {
1811 "already_exists": {
1812 "type": "boolean"
1813 },
1814 "upload_url": {
1815 "type": "string"
1816 }
1817 }
1818 },
1819 "ChangeEntry": {
1820 "type": "object",
1821 "required": [
1822 "table",
1823 "op",
1824 "row_id",
1825 "timestamp"
1826 ],
1827 "properties": {
1828 "data": {},
1829 "op": {
1830 "type": "string"
1831 },
1832 "row_id": {
1833 "type": "string"
1834 },
1835 "table": {
1836 "type": "string"
1837 },
1838 "timestamp": {
1839 "type": "string"
1840 }
1841 }
1842 },
1843 "CompleteRotationRequest": {
1844 "type": "object",
1845 "required": [
1846 "rotation_id"
1847 ],
1848 "properties": {
1849 "rotation_id": {
1850 "type": "string",
1851 "format": "uuid"
1852 }
1853 }
1854 },
1855 "DeactivateKeyRequest": {
1856 "type": "object",
1857 "description": "JSON input for deactivating a machine.",
1858 "required": [
1859 "key",
1860 "machine_id"
1861 ],
1862 "properties": {
1863 "key": {
1864 "type": "string"
1865 },
1866 "machine_id": {
1867 "type": "string"
1868 }
1869 }
1870 },
1871 "DeactivateKeyResponse": {
1872 "type": "object",
1873 "description": "JSON response for key deactivation.",
1874 "required": [
1875 "success",
1876 "message"
1877 ],
1878 "properties": {
1879 "message": {
1880 "type": "string"
1881 },
1882 "success": {
1883 "type": "boolean"
1884 }
1885 }
1886 },
1887 "GetKeyResponse": {
1888 "type": "object",
1889 "required": [
1890 "encrypted_key",
1891 "key_version",
1892 "key_id"
1893 ],
1894 "properties": {
1895 "encrypted_key": {
1896 "type": "string"
1897 },
1898 "key_id": {
1899 "type": "integer",
1900 "format": "int32",
1901 "description": "Current active key identifier."
1902 },
1903 "key_version": {
1904 "type": "integer",
1905 "format": "int32"
1906 },
1907 "pending_key": {
1908 "oneOf": [
1909 {
1910 "type": "null"
1911 },
1912 {
1913 "$ref": "#/components/schemas/PendingKeyInfo",
1914 "description": "If a rotation is in progress, the new key envelope and its key_id."
1915 }
1916 ]
1917 }
1918 }
1919 },
1920 "KeyStatusLicense": {
1921 "type": "object",
1922 "description": "License details within a status response.",
1923 "required": [
1924 "item_id",
1925 "activation_count",
1926 "created_at"
1927 ],
1928 "properties": {
1929 "activation_count": {
1930 "type": "integer",
1931 "format": "int32"
1932 },
1933 "created_at": {
1934 "type": "string"
1935 },
1936 "item_id": {
1937 "type": "string"
1938 },
1939 "max_activations": {
1940 "type": [
1941 "integer",
1942 "null"
1943 ],
1944 "format": "int32"
1945 },
1946 "remaining_activations": {
1947 "type": [
1948 "integer",
1949 "null"
1950 ],
1951 "format": "int32"
1952 }
1953 }
1954 },
1955 "KeyStatusRequest": {
1956 "type": "object",
1957 "description": "Request body for the POST status check.",
1958 "required": [
1959 "key"
1960 ],
1961 "properties": {
1962 "key": {
1963 "type": "string"
1964 }
1965 }
1966 },
1967 "KeyStatusResponse": {
1968 "type": "object",
1969 "description": "JSON response for key status check.",
1970 "required": [
1971 "valid"
1972 ],
1973 "properties": {
1974 "error": {
1975 "type": [
1976 "string",
1977 "null"
1978 ]
1979 },
1980 "license": {
1981 "oneOf": [
1982 {
1983 "type": "null"
1984 },
1985 {
1986 "$ref": "#/components/schemas/KeyStatusLicense"
1987 }
1988 ]
1989 },
1990 "valid": {
1991 "type": "boolean"
1992 }
1993 }
1994 },
1995 "LicenseDeactivateRequest": {
1996 "type": "object",
1997 "description": "JSON input for license deactivation (phone-home).",
1998 "required": [
1999 "key",
2000 "machine_fingerprint"
2001 ],
2002 "properties": {
2003 "key": {
2004 "type": "string"
2005 },
2006 "machine_fingerprint": {
2007 "type": "string"
2008 }
2009 }
2010 },
2011 "LicenseVerifyRequest": {
2012 "type": "object",
2013 "description": "JSON input for license verification (phone-home).",
2014 "required": [
2015 "key",
2016 "machine_fingerprint"
2017 ],
2018 "properties": {
2019 "key": {
2020 "type": "string"
2021 },
2022 "machine_fingerprint": {
2023 "type": "string"
2024 }
2025 }
2026 },
2027 "LicenseVerifyResponse": {
2028 "type": "object",
2029 "description": "JSON response for license verification.",
2030 "required": [
2031 "valid"
2032 ],
2033 "properties": {
2034 "error": {
2035 "type": [
2036 "string",
2037 "null"
2038 ]
2039 },
2040 "expires_in": {
2041 "type": [
2042 "integer",
2043 "null"
2044 ],
2045 "format": "int64"
2046 },
2047 "token": {
2048 "type": [
2049 "string",
2050 "null"
2051 ]
2052 },
2053 "valid": {
2054 "type": "boolean"
2055 }
2056 }
2057 },
2058 "NamespaceEntry": {
2059 "type": "object",
2060 "description": "One notes namespace as the repository holds it.",
2061 "required": [
2062 "name",
2063 "git_ref",
2064 "tip",
2065 "notes"
2066 ],
2067 "properties": {
2068 "git_ref": {
2069 "type": "string",
2070 "description": "The ref it lives on, for a caller assembling a fetch refspec."
2071 },
2072 "name": {
2073 "type": "string",
2074 "description": "Namespace as a person says it: `commits`, `review/security`."
2075 },
2076 "notes": {
2077 "type": "integer",
2078 "format": "int64",
2079 "description": "Notes in the namespace."
2080 },
2081 "tip": {
2082 "type": "string",
2083 "description": "Object id the ref points at."
2084 }
2085 }
2086 },
2087 "NamespacesResponse": {
2088 "type": "object",
2089 "required": [
2090 "data"
2091 ],
2092 "properties": {
2093 "data": {
2094 "type": "array",
2095 "items": {
2096 "$ref": "#/components/schemas/NamespaceEntry"
2097 }
2098 }
2099 }
2100 },
2101 "NoteAttribution": {
2102 "type": "object",
2103 "description": "Who wrote the note and when, from the notes ref's own history.",
2104 "required": [
2105 "commit",
2106 "name",
2107 "email",
2108 "at",
2109 "exact"
2110 ],
2111 "properties": {
2112 "at": {
2113 "type": "string",
2114 "format": "date-time"
2115 },
2116 "commit": {
2117 "type": "string",
2118 "description": "The notes commit that set the note to what it says now."
2119 },
2120 "email": {
2121 "type": "string"
2122 },
2123 "exact": {
2124 "type": "boolean",
2125 "description": "False when the bounded walk ran out before finding the change, so the\ncommit named is as far back as it looked rather than the one responsible."
2126 },
2127 "name": {
2128 "type": "string"
2129 }
2130 }
2131 },
2132 "NoteResponse": {
2133 "type": "object",
2134 "required": [
2135 "namespace",
2136 "target",
2137 "blob",
2138 "content"
2139 ],
2140 "properties": {
2141 "attribution": {
2142 "oneOf": [
2143 {
2144 "type": "null"
2145 },
2146 {
2147 "$ref": "#/components/schemas/NoteAttribution",
2148 "description": "Present only when the request asked for it."
2149 }
2150 ]
2151 },
2152 "blob": {
2153 "type": "string",
2154 "description": "The blob holding the content."
2155 },
2156 "content": {
2157 "type": "string",
2158 "description": "Note content. Note bodies are bytes, not text; anything that is not UTF-8\nis replaced rather than rejected, since a note git accepted has to be\nreadable here."
2159 },
2160 "namespace": {
2161 "type": "string"
2162 },
2163 "target": {
2164 "type": "string",
2165 "description": "The annotated object. Need not be a commit: notes on blobs and trees are\nlegal and this returns them."
2166 }
2167 }
2168 },
2169 "PendingKeyInfo": {
2170 "type": "object",
2171 "required": [
2172 "encrypted_key",
2173 "key_id"
2174 ],
2175 "properties": {
2176 "encrypted_key": {
2177 "type": "string"
2178 },
2179 "key_id": {
2180 "type": "integer",
2181 "format": "int32"
2182 }
2183 }
2184 },
2185 "PullChangeEntry": {
2186 "type": "object",
2187 "required": [
2188 "seq",
2189 "device_id",
2190 "table",
2191 "op",
2192 "row_id",
2193 "timestamp"
2194 ],
2195 "properties": {
2196 "data": {},
2197 "device_id": {
2198 "type": "string"
2199 },
2200 "gck_version": {
2201 "type": [
2202 "integer",
2203 "null"
2204 ],
2205 "format": "int32",
2206 "description": "For a group entry, the GCK generation its ciphertext is sealed under. The\nmember resolves that generation's grant to decrypt it, which is how entries\nwritten before a rotation stay readable. Absent on personal entries, which\nkey off `key_id` instead."
2207 },
2208 "key_id": {
2209 "type": [
2210 "integer",
2211 "null"
2212 ],
2213 "format": "int32",
2214 "description": "Which encryption key was used. Null means key_id 1 (pre-rotation)."
2215 },
2216 "op": {
2217 "type": "string"
2218 },
2219 "row_id": {
2220 "type": "string"
2221 },
2222 "seq": {
2223 "type": "integer",
2224 "format": "int64"
2225 },
2226 "table": {
2227 "type": "string"
2228 },
2229 "timestamp": {
2230 "type": "string"
2231 }
2232 }
2233 },
2234 "PullRequest": {
2235 "type": "object",
2236 "required": [
2237 "device_id",
2238 "cursor"
2239 ],
2240 "properties": {
2241 "cursor": {
2242 "type": "integer",
2243 "format": "int64"
2244 },
2245 "device_id": {
2246 "type": "string"
2247 },
2248 "since": {
2249 "type": [
2250 "string",
2251 "null"
2252 ],
2253 "description": "Optional timestamp filter; only return entries at or after this time."
2254 },
2255 "tables": {
2256 "type": [
2257 "array",
2258 "null"
2259 ],
2260 "items": {
2261 "type": "string"
2262 },
2263 "description": "Optional table name filter; only return entries for these tables."
2264 }
2265 }
2266 },
2267 "PullResponse": {
2268 "type": "object",
2269 "required": [
2270 "changes",
2271 "cursor",
2272 "has_more"
2273 ],
2274 "properties": {
2275 "changes": {
2276 "type": "array",
2277 "items": {
2278 "$ref": "#/components/schemas/PullChangeEntry"
2279 }
2280 },
2281 "cursor": {
2282 "type": "integer",
2283 "format": "int64"
2284 },
2285 "has_more": {
2286 "type": "boolean"
2287 }
2288 }
2289 },
2290 "PushRequest": {
2291 "type": "object",
2292 "required": [
2293 "device_id",
2294 "batch_id",
2295 "changes"
2296 ],
2297 "properties": {
2298 "batch_id": {
2299 "type": "string",
2300 "format": "uuid",
2301 "description": "Client-generated UUID for idempotent push. If a push with the same\nbatch_id has already been committed, the server returns the existing\ncursor without re-inserting."
2302 },
2303 "changes": {
2304 "type": "array",
2305 "items": {
2306 "$ref": "#/components/schemas/ChangeEntry"
2307 }
2308 },
2309 "device_id": {
2310 "type": "string"
2311 }
2312 }
2313 },
2314 "PushResponse": {
2315 "type": "object",
2316 "required": [
2317 "cursor"
2318 ],
2319 "properties": {
2320 "cursor": {
2321 "type": "integer",
2322 "format": "int64"
2323 }
2324 }
2325 },
2326 "PutKeyRequest": {
2327 "type": "object",
2328 "required": [
2329 "encrypted_key",
2330 "expected_version"
2331 ],
2332 "properties": {
2333 "encrypted_key": {
2334 "type": "string"
2335 },
2336 "expected_version": {
2337 "type": "integer",
2338 "format": "int32",
2339 "description": "Expected key version for optimistic concurrency control.\nServer rejects with 409 Conflict if the current version doesn't match."
2340 }
2341 }
2342 },
2343 "PutNoteRequest": {
2344 "type": "object",
2345 "required": [
2346 "content"
2347 ],
2348 "properties": {
2349 "content": {
2350 "type": "string",
2351 "description": "The note body. Trailing whitespace is trimmed and a newline appended, the\nsame shape git's own notes carry."
2352 },
2353 "namespace": {
2354 "type": [
2355 "string",
2356 "null"
2357 ]
2358 }
2359 }
2360 },
2361 "RegisterDeviceRequest": {
2362 "type": "object",
2363 "required": [
2364 "device_name",
2365 "platform"
2366 ],
2367 "properties": {
2368 "device_name": {
2369 "type": "string"
2370 },
2371 "platform": {
2372 "type": "string"
2373 }
2374 }
2375 },
2376 "RotationBatchEntry": {
2377 "type": "object",
2378 "required": [
2379 "seq"
2380 ],
2381 "properties": {
2382 "data": {},
2383 "seq": {
2384 "type": "integer",
2385 "format": "int64"
2386 }
2387 }
2388 },
2389 "RotationBatchRequest": {
2390 "type": "object",
2391 "required": [
2392 "rotation_id",
2393 "entries"
2394 ],
2395 "properties": {
2396 "entries": {
2397 "type": "array",
2398 "items": {
2399 "$ref": "#/components/schemas/RotationBatchEntry"
2400 }
2401 },
2402 "rotation_id": {
2403 "type": "string",
2404 "format": "uuid"
2405 }
2406 }
2407 },
2408 "RotationBatchResponse": {
2409 "type": "object",
2410 "required": [
2411 "updated_count"
2412 ],
2413 "properties": {
2414 "updated_count": {
2415 "type": "integer",
2416 "format": "int64",
2417 "minimum": 0
2418 }
2419 }
2420 },
2421 "RotationEntriesRequest": {
2422 "type": "object",
2423 "required": [
2424 "rotation_id",
2425 "after_seq"
2426 ],
2427 "properties": {
2428 "after_seq": {
2429 "type": "integer",
2430 "format": "int64"
2431 },
2432 "rotation_id": {
2433 "type": "string",
2434 "format": "uuid"
2435 }
2436 }
2437 },
2438 "RotationEntriesResponse": {
2439 "type": "object",
2440 "required": [
2441 "entries",
2442 "has_more"
2443 ],
2444 "properties": {
2445 "entries": {
2446 "type": "array",
2447 "items": {
2448 "$ref": "#/components/schemas/RotationEntry"
2449 }
2450 },
2451 "has_more": {
2452 "type": "boolean"
2453 }
2454 }
2455 },
2456 "RotationEntry": {
2457 "type": "object",
2458 "required": [
2459 "seq",
2460 "table",
2461 "row_id"
2462 ],
2463 "properties": {
2464 "data": {},
2465 "row_id": {
2466 "type": "string"
2467 },
2468 "seq": {
2469 "type": "integer",
2470 "format": "int64"
2471 },
2472 "table": {
2473 "type": "string",
2474 "description": "Source table and row id, echoed so the client can recompute the entry's\nAEAD associated data when re-encrypting under the new key."
2475 }
2476 }
2477 },
2478 "SearchHit": {
2479 "type": "object",
2480 "description": "One search hit, out of the index.",
2481 "required": [
2482 "namespace",
2483 "target",
2484 "blob",
2485 "content",
2486 "target_is_commit",
2487 "summary",
2488 "updated_at",
2489 "updated_by"
2490 ],
2491 "properties": {
2492 "blob": {
2493 "type": "string"
2494 },
2495 "content": {
2496 "type": "string"
2497 },
2498 "namespace": {
2499 "type": "string"
2500 },
2501 "summary": {
2502 "type": "string"
2503 },
2504 "target": {
2505 "type": "string"
2506 },
2507 "target_is_commit": {
2508 "type": "boolean",
2509 "description": "Whether the annotated object is a commit. False for a note on a blob or\na tree, where `summary` and `time` are empty."
2510 },
2511 "time": {
2512 "type": [
2513 "string",
2514 "null"
2515 ],
2516 "format": "date-time"
2517 },
2518 "updated_at": {
2519 "type": "string",
2520 "format": "date-time"
2521 },
2522 "updated_by": {
2523 "type": "string"
2524 }
2525 }
2526 },
2527 "SearchResponse": {
2528 "type": "object",
2529 "required": [
2530 "data",
2531 "indexed"
2532 ],
2533 "properties": {
2534 "data": {
2535 "type": "array",
2536 "items": {
2537 "$ref": "#/components/schemas/SearchHit"
2538 }
2539 },
2540 "indexed": {
2541 "type": "boolean",
2542 "description": "False when the index has never seen this repository, which makes an\nempty `data` mean \"not searchable yet\" rather than \"no matches\". The\nrepository still holds its notes and every other endpoint here returns\nthem; only search needs the index."
2543 }
2544 }
2545 },
2546 "SyncAccountResponse": {
2547 "type": "object",
2548 "required": [
2549 "email",
2550 "username"
2551 ],
2552 "properties": {
2553 "email": {
2554 "type": "string"
2555 },
2556 "username": {
2557 "type": "string"
2558 }
2559 }
2560 },
2561 "SyncAuthRequest": {
2562 "type": "object",
2563 "required": [
2564 "email",
2565 "password",
2566 "api_key",
2567 "key"
2568 ],
2569 "properties": {
2570 "api_key": {
2571 "type": "string"
2572 },
2573 "email": {
2574 "type": "string"
2575 },
2576 "key": {
2577 "type": "string",
2578 "description": "Developer-defined SDK key. Identifies which billing slot this session's\nuploads count against. Required."
2579 },
2580 "password": {
2581 "type": "string"
2582 }
2583 }
2584 },
2585 "SyncAuthResponse": {
2586 "type": "object",
2587 "required": [
2588 "token",
2589 "user_id",
2590 "app_id"
2591 ],
2592 "properties": {
2593 "app_id": {
2594 "type": "string"
2595 },
2596 "token": {
2597 "type": "string"
2598 },
2599 "user_id": {
2600 "type": "string"
2601 }
2602 }
2603 },
2604 "SyncCapChangeRequest": {
2605 "type": "object",
2606 "description": "Request body for `POST /api/v1/sync/subscription/storage-cap`, queues a\ncap change that applies at the next billing cycle.",
2607 "required": [
2608 "cap_bytes"
2609 ],
2610 "properties": {
2611 "cap_bytes": {
2612 "type": "integer",
2613 "format": "int64"
2614 }
2615 }
2616 },
2617 "SyncCheckoutResponse": {
2618 "type": "object",
2619 "required": [
2620 "checkout_url"
2621 ],
2622 "properties": {
2623 "checkout_url": {
2624 "type": "string"
2625 }
2626 }
2627 },
2628 "SyncDeviceResponse": {
2629 "type": "object",
2630 "required": [
2631 "id",
2632 "app_id",
2633 "user_id",
2634 "device_name",
2635 "platform",
2636 "last_seen_at",
2637 "created_at"
2638 ],
2639 "properties": {
2640 "app_id": {
2641 "type": "string"
2642 },
2643 "created_at": {
2644 "type": "string"
2645 },
2646 "device_name": {
2647 "type": "string"
2648 },
2649 "id": {
2650 "type": "string"
2651 },
2652 "last_seen_at": {
2653 "type": "string"
2654 },
2655 "platform": {
2656 "type": "string"
2657 },
2658 "user_id": {
2659 "type": "string"
2660 }
2661 }
2662 },
2663 "SyncQuoteRequest": {
2664 "type": "object",
2665 "description": "Request body for `POST /api/v1/sync/subscription/quote`.",
2666 "required": [
2667 "cap_bytes",
2668 "interval"
2669 ],
2670 "properties": {
2671 "cap_bytes": {
2672 "type": "integer",
2673 "format": "int64"
2674 },
2675 "interval": {
2676 "type": "string"
2677 }
2678 }
2679 },
2680 "SyncQuoteResponse": {
2681 "type": "object",
2682 "required": [
2683 "cap_bytes",
2684 "interval",
2685 "price_cents"
2686 ],
2687 "properties": {
2688 "cap_bytes": {
2689 "type": "integer",
2690 "format": "int64"
2691 },
2692 "interval": {
2693 "type": "string"
2694 },
2695 "price_cents": {
2696 "type": "integer",
2697 "format": "int64"
2698 }
2699 }
2700 },
2701 "SyncStatusResponse": {
2702 "type": "object",
2703 "required": [
2704 "total_changes"
2705 ],
2706 "properties": {
2707 "latest_cursor": {
2708 "type": [
2709 "integer",
2710 "null"
2711 ],
2712 "format": "int64"
2713 },
2714 "total_changes": {
2715 "type": "integer",
2716 "format": "int64"
2717 }
2718 }
2719 },
2720 "SyncSubscribeRequest": {
2721 "type": "object",
2722 "description": "Request body for `POST /api/v1/sync/subscription/checkout`.",
2723 "required": [
2724 "cap_bytes",
2725 "interval"
2726 ],
2727 "properties": {
2728 "cap_bytes": {
2729 "type": "integer",
2730 "format": "int64"
2731 },
2732 "interval": {
2733 "type": "string",
2734 "description": "\"monthly\" or \"annual\"."
2735 }
2736 }
2737 },
2738 "SyncSubscriptionStatusResponse": {
2739 "type": "object",
2740 "description": "Status of the authenticated user's subscription to this app's cloud sync.\nShape matches `synckit_client::SubscriptionStatus`.",
2741 "required": [
2742 "active"
2743 ],
2744 "properties": {
2745 "active": {
2746 "type": "boolean"
2747 },
2748 "current_period_end": {
2749 "type": [
2750 "string",
2751 "null"
2752 ]
2753 },
2754 "pending_storage_limit_bytes": {
2755 "type": [
2756 "integer",
2757 "null"
2758 ],
2759 "format": "int64",
2760 "description": "Queued storage cap, applied at the next billing cycle. `None` when no\nchange is pending."
2761 },
2762 "status": {
2763 "type": [
2764 "string",
2765 "null"
2766 ]
2767 },
2768 "storage_limit_bytes": {
2769 "type": [
2770 "integer",
2771 "null"
2772 ],
2773 "format": "int64"
2774 },
2775 "storage_used_bytes": {
2776 "type": [
2777 "integer",
2778 "null"
2779 ],
2780 "format": "int64"
2781 },
2782 "tier": {
2783 "type": [
2784 "string",
2785 "null"
2786 ],
2787 "description": "Billing interval (\"monthly\" / \"annual\"). Kept under the legacy `tier`\nkey for client SDK backwards compatibility."
2788 }
2789 }
2790 },
2791 "ValidateAppQuery": {
2792 "type": "object",
2793 "required": [
2794 "api_key"
2795 ],
2796 "properties": {
2797 "api_key": {
2798 "type": "string"
2799 }
2800 }
2801 },
2802 "ValidateAppResponse": {
2803 "type": "object",
2804 "required": [
2805 "app_name"
2806 ],
2807 "properties": {
2808 "app_name": {
2809 "type": "string"
2810 }
2811 }
2812 },
2813 "ValidateKeyLicense": {
2814 "type": "object",
2815 "description": "License details within a validation response.",
2816 "required": [
2817 "item_id",
2818 "activation_count",
2819 "created_at"
2820 ],
2821 "properties": {
2822 "activation_count": {
2823 "type": "integer",
2824 "format": "int32"
2825 },
2826 "created_at": {
2827 "type": "string"
2828 },
2829 "item_id": {
2830 "type": "string"
2831 },
2832 "max_activations": {
2833 "type": [
2834 "integer",
2835 "null"
2836 ],
2837 "format": "int32"
2838 }
2839 }
2840 },
2841 "ValidateKeyRequest": {
2842 "type": "object",
2843 "description": "JSON input for validating/activating a license key.",
2844 "required": [
2845 "key",
2846 "machine_id"
2847 ],
2848 "properties": {
2849 "key": {
2850 "type": "string"
2851 },
2852 "label": {
2853 "type": [
2854 "string",
2855 "null"
2856 ]
2857 },
2858 "machine_id": {
2859 "type": "string"
2860 }
2861 }
2862 },
2863 "ValidateKeyResponse": {
2864 "type": "object",
2865 "description": "JSON response for key validation/activation.",
2866 "required": [
2867 "valid"
2868 ],
2869 "properties": {
2870 "activated": {
2871 "type": [
2872 "boolean",
2873 "null"
2874 ]
2875 },
2876 "error": {
2877 "type": [
2878 "string",
2879 "null"
2880 ]
2881 },
2882 "license": {
2883 "oneOf": [
2884 {
2885 "type": "null"
2886 },
2887 {
2888 "$ref": "#/components/schemas/ValidateKeyLicense"
2889 }
2890 ]
2891 },
2892 "valid": {
2893 "type": "boolean"
2894 }
2895 }
2896 },
2897 "WriteResponse": {
2898 "type": "object",
2899 "description": "What a write did.",
2900 "required": [
2901 "namespace",
2902 "target",
2903 "status",
2904 "merged"
2905 ],
2906 "properties": {
2907 "merged": {
2908 "type": "boolean",
2909 "description": "Somebody else annotated the same target while this write was in flight\nand the two were merged, so the stored note is not byte-for-byte what was\nsent. Re-read it if that matters."
2910 },
2911 "namespace": {
2912 "type": "string"
2913 },
2914 "status": {
2915 "type": "string",
2916 "description": "`written` when the ref moved, `unchanged` when the namespace already said\nexactly this. Re-putting an identical note is not an error and costs no\ncommit."
2917 },
2918 "target": {
2919 "type": "string"
2920 },
2921 "tip": {
2922 "type": [
2923 "string",
2924 "null"
2925 ],
2926 "description": "Where the namespace points now, absent when nothing was written."
2927 }
2928 }
2929 }
2930 }
2931 },
2932 "security": [
2933 {
2934 "bearer": []
2935 }
2936 ],
2937 "tags": [
2938 {
2939 "name": "License Keys",
2940 "description": "Public license key validation, activation, and deactivation. Stable API: response shapes are frozen."
2941 },
2942 {
2943 "name": "SyncKit",
2944 "description": "E2E encrypted cloud sync for indie apps. JWT auth via /api/v1/sync/auth, then Bearer token on all other endpoints."
2945 },
2946 {
2947 "name": "Git Notes",
2948 "description": "Read and write refs/notes/* on a repository. Reads answer from the repository and take a session or a personal access token; writes take a push-scoped personal access token as HTTP Basic auth, the same credential git push uses."
2949 }
2950 ]
2951 }
2952