@brass-build/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/AGENTS.md +255 -0
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +836 -0
  5. package/dist/api-fetch.d.ts +16 -0
  6. package/dist/api-fetch.d.ts.map +1 -0
  7. package/dist/api-fetch.js +97 -0
  8. package/dist/api-fetch.js.map +1 -0
  9. package/dist/api-headers.d.ts +3 -0
  10. package/dist/api-headers.d.ts.map +1 -0
  11. package/dist/api-headers.js +25 -0
  12. package/dist/api-headers.js.map +1 -0
  13. package/dist/auth-flow.d.ts +23 -0
  14. package/dist/auth-flow.d.ts.map +1 -0
  15. package/dist/auth-flow.js +76 -0
  16. package/dist/auth-flow.js.map +1 -0
  17. package/dist/client.d.ts +106 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +1278 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/doc-url-sync.d.ts +19 -0
  22. package/dist/doc-url-sync.d.ts.map +1 -0
  23. package/dist/doc-url-sync.js +181 -0
  24. package/dist/doc-url-sync.js.map +1 -0
  25. package/dist/document-branches.d.ts +32 -0
  26. package/dist/document-branches.d.ts.map +1 -0
  27. package/dist/document-branches.js +89 -0
  28. package/dist/document-branches.js.map +1 -0
  29. package/dist/document-checkpoints.d.ts +24 -0
  30. package/dist/document-checkpoints.d.ts.map +1 -0
  31. package/dist/document-checkpoints.js +115 -0
  32. package/dist/document-checkpoints.js.map +1 -0
  33. package/dist/document-commands.d.ts +93 -0
  34. package/dist/document-commands.d.ts.map +1 -0
  35. package/dist/document-commands.js +423 -0
  36. package/dist/document-commands.js.map +1 -0
  37. package/dist/document-signals.d.ts +53 -0
  38. package/dist/document-signals.d.ts.map +1 -0
  39. package/dist/document-signals.js +114 -0
  40. package/dist/document-signals.js.map +1 -0
  41. package/dist/document-streams.d.ts +22 -0
  42. package/dist/document-streams.d.ts.map +1 -0
  43. package/dist/document-streams.js +98 -0
  44. package/dist/document-streams.js.map +1 -0
  45. package/dist/document-thumbnail-capture.d.ts +27 -0
  46. package/dist/document-thumbnail-capture.d.ts.map +1 -0
  47. package/dist/document-thumbnail-capture.js +160 -0
  48. package/dist/document-thumbnail-capture.js.map +1 -0
  49. package/dist/documents-namespace.d.ts +134 -0
  50. package/dist/documents-namespace.d.ts.map +1 -0
  51. package/dist/documents-namespace.js +190 -0
  52. package/dist/documents-namespace.js.map +1 -0
  53. package/dist/documents.d.ts +146 -0
  54. package/dist/documents.d.ts.map +1 -0
  55. package/dist/documents.js +2045 -0
  56. package/dist/documents.js.map +1 -0
  57. package/dist/emitter.d.ts +15 -0
  58. package/dist/emitter.d.ts.map +1 -0
  59. package/dist/emitter.js +90 -0
  60. package/dist/emitter.js.map +1 -0
  61. package/dist/errors.d.ts +18 -0
  62. package/dist/errors.d.ts.map +1 -0
  63. package/dist/errors.js +22 -0
  64. package/dist/errors.js.map +1 -0
  65. package/dist/export-guest.d.ts +9 -0
  66. package/dist/export-guest.d.ts.map +1 -0
  67. package/dist/export-guest.js +225 -0
  68. package/dist/export-guest.js.map +1 -0
  69. package/dist/fetch-retry.d.ts +16 -0
  70. package/dist/fetch-retry.d.ts.map +1 -0
  71. package/dist/fetch-retry.js +90 -0
  72. package/dist/fetch-retry.js.map +1 -0
  73. package/dist/guest-link.d.ts +3 -0
  74. package/dist/guest-link.d.ts.map +1 -0
  75. package/dist/guest-link.js +13 -0
  76. package/dist/guest-link.js.map +1 -0
  77. package/dist/handles.d.ts +57 -0
  78. package/dist/handles.d.ts.map +1 -0
  79. package/dist/handles.js +1375 -0
  80. package/dist/handles.js.map +1 -0
  81. package/dist/hosted-runtime.d.ts +33 -0
  82. package/dist/hosted-runtime.d.ts.map +1 -0
  83. package/dist/hosted-runtime.js +99 -0
  84. package/dist/hosted-runtime.js.map +1 -0
  85. package/dist/hosted-ui.d.ts +84 -0
  86. package/dist/hosted-ui.d.ts.map +1 -0
  87. package/dist/hosted-ui.js +673 -0
  88. package/dist/hosted-ui.js.map +1 -0
  89. package/dist/importer.d.ts +22 -0
  90. package/dist/importer.d.ts.map +1 -0
  91. package/dist/importer.js +54 -0
  92. package/dist/importer.js.map +1 -0
  93. package/dist/index.d.ts +54 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +132 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/init-keys.d.ts +3 -0
  98. package/dist/init-keys.d.ts.map +1 -0
  99. package/dist/init-keys.js +13 -0
  100. package/dist/init-keys.js.map +1 -0
  101. package/dist/internal.d.ts +6 -0
  102. package/dist/internal.d.ts.map +1 -0
  103. package/dist/internal.js +17 -0
  104. package/dist/internal.js.map +1 -0
  105. package/dist/jwt.d.ts +3 -0
  106. package/dist/jwt.d.ts.map +1 -0
  107. package/dist/jwt.js +22 -0
  108. package/dist/jwt.js.map +1 -0
  109. package/dist/launch-context.d.ts +4 -0
  110. package/dist/launch-context.d.ts.map +1 -0
  111. package/dist/launch-context.js +44 -0
  112. package/dist/launch-context.js.map +1 -0
  113. package/dist/login-hint.d.ts +4 -0
  114. package/dist/login-hint.d.ts.map +1 -0
  115. package/dist/login-hint.js +63 -0
  116. package/dist/login-hint.js.map +1 -0
  117. package/dist/mutate-batching.d.ts +14 -0
  118. package/dist/mutate-batching.d.ts.map +1 -0
  119. package/dist/mutate-batching.js +65 -0
  120. package/dist/mutate-batching.js.map +1 -0
  121. package/dist/open-document.d.ts +127 -0
  122. package/dist/open-document.d.ts.map +1 -0
  123. package/dist/open-document.js +2121 -0
  124. package/dist/open-document.js.map +1 -0
  125. package/dist/page-title.d.ts +3 -0
  126. package/dist/page-title.d.ts.map +1 -0
  127. package/dist/page-title.js +32 -0
  128. package/dist/page-title.js.map +1 -0
  129. package/dist/perf.d.ts +9 -0
  130. package/dist/perf.d.ts.map +1 -0
  131. package/dist/perf.js +39 -0
  132. package/dist/perf.js.map +1 -0
  133. package/dist/presence.d.ts +62 -0
  134. package/dist/presence.d.ts.map +1 -0
  135. package/dist/presence.js +331 -0
  136. package/dist/presence.js.map +1 -0
  137. package/dist/record-paging.d.ts +99 -0
  138. package/dist/record-paging.d.ts.map +1 -0
  139. package/dist/record-paging.js +220 -0
  140. package/dist/record-paging.js.map +1 -0
  141. package/dist/schema-conflict-dialog.d.ts +10 -0
  142. package/dist/schema-conflict-dialog.d.ts.map +1 -0
  143. package/dist/schema-conflict-dialog.js +164 -0
  144. package/dist/schema-conflict-dialog.js.map +1 -0
  145. package/dist/selection-persistence.d.ts +5 -0
  146. package/dist/selection-persistence.d.ts.map +1 -0
  147. package/dist/selection-persistence.js +90 -0
  148. package/dist/selection-persistence.js.map +1 -0
  149. package/dist/serializer.d.ts +3 -0
  150. package/dist/serializer.d.ts.map +1 -0
  151. package/dist/serializer.js +2 -0
  152. package/dist/serializer.js.map +1 -0
  153. package/dist/session-persistence.d.ts +4 -0
  154. package/dist/session-persistence.d.ts.map +1 -0
  155. package/dist/session-persistence.js +68 -0
  156. package/dist/session-persistence.js.map +1 -0
  157. package/dist/session-toolbar.d.ts +62 -0
  158. package/dist/session-toolbar.d.ts.map +1 -0
  159. package/dist/session-toolbar.js +1205 -0
  160. package/dist/session-toolbar.js.map +1 -0
  161. package/dist/session.d.ts +69 -0
  162. package/dist/session.d.ts.map +1 -0
  163. package/dist/session.js +926 -0
  164. package/dist/session.js.map +1 -0
  165. package/dist/tail-ops.d.ts +28 -0
  166. package/dist/tail-ops.d.ts.map +1 -0
  167. package/dist/tail-ops.js +46 -0
  168. package/dist/tail-ops.js.map +1 -0
  169. package/dist/topic-lifecycle.d.ts +41 -0
  170. package/dist/topic-lifecycle.d.ts.map +1 -0
  171. package/dist/topic-lifecycle.js +169 -0
  172. package/dist/topic-lifecycle.js.map +1 -0
  173. package/dist/types.d.ts +22 -0
  174. package/dist/types.d.ts.map +1 -0
  175. package/dist/types.js +2 -0
  176. package/dist/types.js.map +1 -0
  177. package/dist/undo.d.ts +18 -0
  178. package/dist/undo.d.ts.map +1 -0
  179. package/dist/undo.js +110 -0
  180. package/dist/undo.js.map +1 -0
  181. package/dist/version-bar.d.ts +22 -0
  182. package/dist/version-bar.d.ts.map +1 -0
  183. package/dist/version-bar.js +169 -0
  184. package/dist/version-bar.js.map +1 -0
  185. package/dist/version.d.ts +3 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +8 -0
  188. package/dist/version.js.map +1 -0
  189. package/dist/vite.d.ts +15 -0
  190. package/dist/vite.d.ts.map +1 -0
  191. package/dist/vite.js +36 -0
  192. package/dist/vite.js.map +1 -0
  193. package/dist/ws-client.d.ts +53 -0
  194. package/dist/ws-client.d.ts.map +1 -0
  195. package/dist/ws-client.js +604 -0
  196. package/dist/ws-client.js.map +1 -0
  197. package/dist/ws-frame-limit.d.ts +21 -0
  198. package/dist/ws-frame-limit.d.ts.map +1 -0
  199. package/dist/ws-frame-limit.js +40 -0
  200. package/dist/ws-frame-limit.js.map +1 -0
  201. package/docs/app-manifest.md +65 -0
  202. package/docs/custom-chrome.md +147 -0
  203. package/docs/hosted-ui-advanced.md +77 -0
  204. package/docs/imported-documents.md +261 -0
  205. package/docs/presence.md +129 -0
  206. package/docs/schemas.md +135 -0
  207. package/docs/streams.md +248 -0
  208. package/package.json +60 -0
  209. package/scripts/prepare.mjs +50 -0
  210. package/src/api-fetch.ts +127 -0
  211. package/src/api-headers.ts +28 -0
  212. package/src/auth-flow.ts +125 -0
  213. package/src/client.ts +1457 -0
  214. package/src/doc-url-sync.ts +183 -0
  215. package/src/document-branches.ts +124 -0
  216. package/src/document-checkpoints.ts +135 -0
  217. package/src/document-commands.ts +651 -0
  218. package/src/document-signals.ts +234 -0
  219. package/src/document-streams.ts +132 -0
  220. package/src/document-thumbnail-capture.ts +191 -0
  221. package/src/documents-namespace.ts +516 -0
  222. package/src/documents.ts +2426 -0
  223. package/src/emitter.ts +129 -0
  224. package/src/errors.ts +45 -0
  225. package/src/export-guest.ts +282 -0
  226. package/src/fetch-retry.ts +119 -0
  227. package/src/guest-link.ts +14 -0
  228. package/src/handles.ts +1731 -0
  229. package/src/hosted-runtime.ts +109 -0
  230. package/src/hosted-ui.ts +1020 -0
  231. package/src/importer.ts +89 -0
  232. package/src/index.ts +267 -0
  233. package/src/init-keys.ts +15 -0
  234. package/src/internal.ts +23 -0
  235. package/src/jwt.ts +18 -0
  236. package/src/launch-context.ts +43 -0
  237. package/src/login-hint.ts +60 -0
  238. package/src/mutate-batching.ts +63 -0
  239. package/src/open-document.ts +2869 -0
  240. package/src/page-title.ts +33 -0
  241. package/src/perf.ts +46 -0
  242. package/src/presence.ts +471 -0
  243. package/src/record-paging.ts +270 -0
  244. package/src/schema-conflict-dialog.ts +176 -0
  245. package/src/selection-persistence.ts +98 -0
  246. package/src/serializer.ts +15 -0
  247. package/src/session-persistence.ts +69 -0
  248. package/src/session-toolbar.ts +1457 -0
  249. package/src/session.ts +1262 -0
  250. package/src/tail-ops.ts +63 -0
  251. package/src/topic-lifecycle.ts +217 -0
  252. package/src/types.ts +75 -0
  253. package/src/undo.ts +125 -0
  254. package/src/version-bar.ts +221 -0
  255. package/src/version.ts +8 -0
  256. package/src/vite.ts +49 -0
  257. package/src/ws-client.ts +678 -0
  258. package/src/ws-frame-limit.ts +52 -0
@@ -0,0 +1,516 @@
1
+ // SDK surface: `Brass.documents.*`. The metadata surface: get /
2
+ // thumbnails / branches. The editable surface (snapshot + ops +
3
+ // mutate) is the `Document` runtime class via `Brass.openDocumentById`
4
+ // / `Brass.createDocument`. Access-granting is hosted-only (the share
5
+ // dialog), not a programmatic method here.
6
+
7
+ import type { Document } from './documents.js';
8
+ import type { ImportClaimResponse } from './importer.js';
9
+ import { createApiFetch } from './api-fetch.js';
10
+ import { BrassError } from './errors.js';
11
+
12
+ export type DocumentRole = 'owner' | 'editor' | 'viewer';
13
+
14
+ export interface DocumentMember {
15
+ sub: string;
16
+ role: DocumentRole;
17
+ granted_at: string;
18
+ }
19
+
20
+ // An app-supplied preview image, anchored to a `(branch, version)`.
21
+ // `url` is a short-lived presigned GET valid until `expires_at`;
22
+ // `width` / `height` are `null` when the uploading app didn't report
23
+ // them. Brass can't render an app's document body, so a thumbnail
24
+ // exists only when the app uploaded one; treat its absence as normal.
25
+ export interface DocumentThumbnail {
26
+ url: string;
27
+ version: number;
28
+ branch_id: string;
29
+ content_type: string;
30
+ width: number | null;
31
+ height: number | null;
32
+ created_at: string;
33
+ expires_at: string;
34
+ }
35
+
36
+ // Wire shape returned by the document HTTP routes. `project_id` is
37
+ // `null` for loose documents.
38
+ export interface DocumentSummary {
39
+ doc_id: string;
40
+ organization_id: string;
41
+ project_id: string | null;
42
+ name: string;
43
+ created_at: string;
44
+ updated_at: string;
45
+ owner_sub: string;
46
+ deleted_at?: string;
47
+ // The document's current card thumbnail, or absent when no app has
48
+ // uploaded one.
49
+ thumbnail?: DocumentThumbnail;
50
+ }
51
+
52
+ // One byte stream of the original file behind an opaque-blob document. A
53
+ // source file is a set of one or more streams: the primary stream (what
54
+ // the file's own name names) plus any sidecar streams uploaded with it (a
55
+ // glTF's `.bin` buffer, a shapefile's `.dbf`). Each stream carries its own
56
+ // short-lived download URL, so an app fetches exactly the streams it
57
+ // needs, in parallel.
58
+ export interface DocumentSourceStream {
59
+ // The stream's name within the file set (the primary stream's is the
60
+ // file's own name).
61
+ name: string;
62
+ url: string;
63
+ content_type: string;
64
+ byte_size: number;
65
+ }
66
+
67
+ // The original file behind an opaque-blob document, returned by
68
+ // `doc.sourceBlob()`. `url` is a short-lived presigned GET the app
69
+ // fetches the raw bytes from (the bytes are not inlined: a real source
70
+ // file routinely exceeds the API response cap); the descriptor fields let
71
+ // the app decide whether it handles this format before downloading.
72
+ // `streams` lists every stream of the file set, primary first (its entry
73
+ // mirrors the top-level fields): a single-stream file has exactly one
74
+ // entry, so an app that iterates `streams` handles single- and
75
+ // multi-stream files with one code path.
76
+ export interface DocumentSourceBlob {
77
+ url: string;
78
+ filename: string;
79
+ content_type: string;
80
+ byte_size: number;
81
+ expires_in: number;
82
+ streams: DocumentSourceStream[];
83
+ }
84
+
85
+ export interface DocumentRecord extends DocumentSummary {
86
+ // The caller's effective role on this document, resolved via the
87
+ // scope-loop on the server.
88
+ role: DocumentRole;
89
+ members: DocumentMember[];
90
+ app_metadata?: Record<string, unknown>;
91
+ // Server-authored, opaque label for where the document lives, rendered
92
+ // verbatim by the toolbar's location chip (the project's name when filed,
93
+ // null when loose). The SDK never composes location from `project_id` /
94
+ // `organization_id`, so the hierarchy above the document can evolve
95
+ // (folders / teams) without a client change.
96
+ location_label: string | null;
97
+ // Whether someone can produce a file from this document: a platform
98
+ // codec, an opaque blob's stored bytes, or an app installed on the
99
+ // document's organization that declares an output for it. The server
100
+ // resolves it, so a newly exportable format needs no client change.
101
+ downloadable: boolean;
102
+ }
103
+
104
+ // One stream of a document: a named, independently-loadable CRDT unit
105
+ // (`doc.stream(name)` opens exactly the streams an app touches, so open
106
+ // cost scales with what is used, not the whole document). `line_id`
107
+ // identifies the stream's underlying CRDT line; apps only need `name`.
108
+ export interface DocumentStreamInfo {
109
+ name: string;
110
+ // The stream's format. `doc.stream()` opens `'crdt'` entries, the
111
+ // editable collaborative unit, and rejects any other value loudly, so a
112
+ // format the platform adds later cannot be half-opened by this SDK.
113
+ format: string;
114
+ line_id: string;
115
+ created_at: string;
116
+ created_by: string;
117
+ }
118
+
119
+ // One branch of a document. A document has N branches: alternate
120
+ // states of the same design (the AEC "design option" used for
121
+ // whole-scheme versioning). Each branch is edited independently;
122
+ // `Brass.openDocumentById(docId, { branch })` opens a specific one.
123
+ export interface BranchSummary {
124
+ branch_id: string;
125
+ name: string;
126
+ created_at: string;
127
+ created_by: string;
128
+ // True for the branch a cold open (no `?branch=`) lands on.
129
+ is_default: boolean;
130
+ // Lineage. Null for the doc's original branch; set on forked ones.
131
+ created_from: { branch_id: string; version: number } | null;
132
+ // The branch's current live head seq.
133
+ current_seq: number;
134
+ }
135
+
136
+ // Input to `documents.setThumbnail`. `image` is the raw image bytes
137
+ // (a canvas `toBlob` result, a fetched `ArrayBuffer`, a `Uint8Array`);
138
+ // the SDK base64-encodes them onto the JSON upload body. `contentType`
139
+ // is required unless `image` is a `Blob` that already carries a
140
+ // `type` (canvas `toBlob` does). Allowed types: image/png,
141
+ // image/jpeg, image/webp. The version the thumbnail depicts and the
142
+ // branch it belongs to are read from the `Document` passed to
143
+ // `setThumbnail`, not supplied here, so the stored anchor always
144
+ // matches the state the app rendered.
145
+ export interface SetThumbnailInput {
146
+ image: Blob | ArrayBuffer | Uint8Array;
147
+ // Image MIME type. Optional only when `image` is a typed `Blob`.
148
+ contentType?: string;
149
+ // Optional intrinsic pixel dimensions, round-tripped so a renderer
150
+ // can reserve layout space before the image loads.
151
+ width?: number;
152
+ height?: number;
153
+ }
154
+
155
+ // Options for `documents.getThumbnail`. With no options the call
156
+ // returns the document's current card thumbnail; pass `version` to
157
+ // fetch the thumbnail for a point in history (the newest one at or
158
+ // below that version).
159
+ export interface GetThumbnailOptions {
160
+ version?: number;
161
+ branch?: string;
162
+ }
163
+
164
+ // Wire shape returned by `POST /documents/:docId/checkpoints` and
165
+ // mirrored on `listDocumentVersions` markers with `kind: 'checkpoint'`.
166
+ // `created_by` / `created_at` are always present.
167
+ export interface DocumentCheckpoint {
168
+ version: number;
169
+ branch_id: string;
170
+ created_by: string;
171
+ created_at: string;
172
+ }
173
+
174
+ // Input to `documents.pushToDrive`. The optional `message` rides as
175
+ // the new drive version's commit message (and surfaces on the
176
+ // `drive.push.success` audit so operators investigating "what
177
+ // changed" can read it). Omitted means no message.
178
+ export interface PushToDriveInput {
179
+ message?: string;
180
+ }
181
+
182
+ // Outcome of a successful push.
183
+ export interface PushToDriveResult {
184
+ // The new external ref (the drive's version id) the push produced.
185
+ // Apps that surface "published as version X" use this. Empty when
186
+ // `pending` is true (the importer has not produced the version yet).
187
+ new_ref: string;
188
+ // True when the drive's write is asynchronous: the bytes were
189
+ // submitted and the drive's importer was queued, but the resulting
190
+ // Version (and the re-synced baseline) is not known yet. This is the
191
+ // file-roundtrip path (a doc imported from a drive file). The doc stays
192
+ // `ahead` until the drive's change-event webhook confirms the import,
193
+ // so an app should say "submitted, waiting for the importer" rather
194
+ // than "published", and expect `driveStatus` to stay `canPush` until
195
+ // the confirmation lands. Absent / false means the push was
196
+ // synchronous and the baseline already advanced (the doc is `synced`).
197
+ pending?: boolean;
198
+ // Entity count the push uploaded.
199
+ entity_count: number;
200
+ // ISO timestamp of the push.
201
+ pushed_at: string;
202
+ }
203
+
204
+ export interface CreateBranchRequest {
205
+ // The version on the source branch to fork from. Omit it to fork
206
+ // from the branch's live head, which is the common case ("branch
207
+ // the current state") and the only race-free one: the server
208
+ // resolves the head atomically, so you never have to read and
209
+ // pass a `current_seq` that is already stale by the time the
210
+ // create lands. Provide a value only to fork from a specific
211
+ // historical version.
212
+ from_version?: number;
213
+ // The branch to fork from. Omit to fork from the doc's default.
214
+ from_branch_id?: string;
215
+ // Optional display name. Defaults to "<source> copy" server-side.
216
+ name?: string;
217
+ }
218
+
219
+ export interface DocumentsNamespace {
220
+ // Metadata read. Returns organization_id, project_id (or null), role,
221
+ // members, name. Closes the cold-URL-restore gap (an app whose URL
222
+ // carries a doc_id can resolve project + organization + role from
223
+ // this one call before opening the editable Document via
224
+ // `Brass.openDocumentById`). Creating / moving / listing documents is
225
+ // done through the hosted pickers + document-settings dialog, not
226
+ // this namespace; the runtime create-and-open path is the top-level
227
+ // `Brass.createDocument` / `Brass.openDocumentById`.
228
+ get(docId: string): Promise<DocumentRecord>;
229
+ // Upload a preview image for a document. Brass cannot render an
230
+ // app's document body, so an app that wants a thumbnail on its
231
+ // document cards renders one itself (e.g. `canvas.toBlob`) and
232
+ // uploads it here. Pass the live `Document` the image was rendered
233
+ // from: the SDK anchors the thumbnail to that document's exact
234
+ // current version and branch, so the stored preview always matches
235
+ // the state it depicts. The most recent upload becomes the
236
+ // document's card thumbnail. Returns the stored thumbnail (with a
237
+ // presigned `url`).
238
+ setThumbnail(
239
+ doc: Document,
240
+ input: SetThumbnailInput,
241
+ ): Promise<DocumentThumbnail>;
242
+ // Read a document's thumbnail. With no options, returns the
243
+ // document's current card thumbnail; pass `version` for the
244
+ // thumbnail at a point in history. Resolves `null` when no
245
+ // matching thumbnail exists.
246
+ getThumbnail(
247
+ docId: string,
248
+ options?: GetThumbnailOptions,
249
+ ): Promise<DocumentThumbnail | null>;
250
+ }
251
+
252
+ // Plumbing behind the per-`Document` surface (`doc.pushToDrive` /
253
+ // `doc.createCheckpoint`), the internal branch controller (list / create,
254
+ // which the toolbar branch command + hosted branch dialog drive), and the
255
+ // hosted history modal; not exposed on `Brass.documents`.
256
+ export interface DocumentsInternalNamespace {
257
+ createCheckpoint(doc: Document): Promise<DocumentCheckpoint>;
258
+ listBranches(
259
+ docId: string,
260
+ ): Promise<{ default_branch_id: string; branches: BranchSummary[] }>;
261
+ // Streams plumbing behind `doc.stream()` /
262
+ // `doc.listStreams()`. A stream's `line_id` is what the SDK joins
263
+ // (`?branch=<line_id>`) and subscribes (`doc:<docId>:<line_id>`).
264
+ listStreams(
265
+ docId: string,
266
+ branchId: string,
267
+ ): Promise<{ branch_id: string; streams: DocumentStreamInfo[] }>;
268
+ createStream(
269
+ docId: string,
270
+ branchId: string,
271
+ name: string,
272
+ ): Promise<DocumentStreamInfo>;
273
+ createBranch(
274
+ docId: string,
275
+ input: CreateBranchRequest,
276
+ ): Promise<BranchSummary>;
277
+ pushToDrive(
278
+ doc: Document,
279
+ input?: PushToDriveInput,
280
+ ): Promise<PushToDriveResult>;
281
+ // Fetch a presigned GET + descriptor for the original file behind an
282
+ // opaque-blob document, so an app that knows the format can download
283
+ // and parse it in-browser. Rejects with a `BrassError` when the doc is
284
+ // not a stored opaque blob or the caller cannot reach it (404). The
285
+ // per-doc instance method `doc.sourceBlob()` routes through this.
286
+ sourceBlob(doc: Document): Promise<DocumentSourceBlob>;
287
+ // The import-claim protocol behind `Brass.registerImporter`. The SDK
288
+ // (never the app) claims the right to run the import of a stored
289
+ // source file, and reports completion so the platform stamps the
290
+ // document with the importing app's schema. Both reject with a
291
+ // `BrassError` on the ineligible / claim-lost outcomes.
292
+ claimImport(docId: string): Promise<ImportClaimResponse>;
293
+ completeImport(
294
+ docId: string,
295
+ ): Promise<{ schema_ref: { app_id: string; schema_hash: string } }>;
296
+ }
297
+
298
+ interface DocumentsFactoryOptions {
299
+ apiBaseUrl: string;
300
+ getAccessToken: () => Promise<string | null>;
301
+ getAppAssertion?: () => Promise<string | null>;
302
+ }
303
+
304
+ export function createDocumentsNamespace(
305
+ opts: DocumentsFactoryOptions,
306
+ ): DocumentsNamespace & DocumentsInternalNamespace {
307
+ const apiFetch = createApiFetch(opts);
308
+
309
+ return {
310
+ get(docId) {
311
+ return apiFetch<DocumentRecord>({
312
+ path: `/documents/${encodeURIComponent(docId)}`,
313
+ });
314
+ },
315
+ listBranches(docId) {
316
+ return apiFetch<{ default_branch_id: string; branches: BranchSummary[] }>({
317
+ path: `/documents/${encodeURIComponent(docId)}/branches`,
318
+ });
319
+ },
320
+ listStreams(docId, branchId) {
321
+ return apiFetch<{ branch_id: string; streams: DocumentStreamInfo[] }>({
322
+ path: `/documents/${encodeURIComponent(docId)}/streams?branch=${encodeURIComponent(branchId)}`,
323
+ });
324
+ },
325
+ createStream(docId, branchId, name) {
326
+ return apiFetch<DocumentStreamInfo>({
327
+ method: 'POST',
328
+ path: `/documents/${encodeURIComponent(docId)}/streams`,
329
+ body: {
330
+ name,
331
+ branch_id: branchId,
332
+ },
333
+ });
334
+ },
335
+ createBranch(docId, input) {
336
+ const body: CreateBranchRequest = {};
337
+ if (input.from_version !== undefined) {
338
+ body.from_version = input.from_version;
339
+ }
340
+ if (input.from_branch_id !== undefined) {
341
+ body.from_branch_id = input.from_branch_id;
342
+ }
343
+ if (input.name !== undefined) body.name = input.name;
344
+ return apiFetch<BranchSummary>({
345
+ method: 'POST',
346
+ path: `/documents/${encodeURIComponent(docId)}/branches`,
347
+ body,
348
+ });
349
+ },
350
+ async setThumbnail(doc, input) {
351
+ const docId = doc.id;
352
+ if (docId === null) {
353
+ throw new BrassError(
354
+ 'setThumbnail requires a Document opened from the server',
355
+ { status: 400 },
356
+ );
357
+ }
358
+ // Capture the depicted version synchronously, before the async
359
+ // encode, so a concurrent editor's op landing mid-upload cannot
360
+ // shift the anchor away from the state the app rendered.
361
+ const version = doc.currentSeq;
362
+ if (version === null) {
363
+ throw new BrassError(
364
+ 'setThumbnail: the document has no resolved version yet',
365
+ { status: 400 },
366
+ );
367
+ }
368
+ const branchId = doc.branchId;
369
+ const { data, contentType } = await encodeThumbnailImage(input);
370
+ const body: {
371
+ content_type: string;
372
+ data: string;
373
+ version: number;
374
+ branch_id?: string;
375
+ width?: number;
376
+ height?: number;
377
+ } = { content_type: contentType, data, version };
378
+ if (branchId !== null) body.branch_id = branchId;
379
+ if (input.width !== undefined) body.width = input.width;
380
+ if (input.height !== undefined) body.height = input.height;
381
+ const res = await apiFetch<{ thumbnail: DocumentThumbnail }>({
382
+ method: 'POST',
383
+ path: `/documents/${encodeURIComponent(docId)}/thumbnail`,
384
+ body,
385
+ });
386
+ return res.thumbnail;
387
+ },
388
+ async createCheckpoint(doc) {
389
+ const docId = doc.id;
390
+ if (docId === null) {
391
+ throw new BrassError(
392
+ 'createCheckpoint requires a Document opened from the server',
393
+ { status: 400 },
394
+ );
395
+ }
396
+ // Capture the depicted version synchronously, so a concurrent
397
+ // editor's op landing mid-call cannot shift the anchor away
398
+ // from the state the user is on. Mirrors the setThumbnail
399
+ // version-capture pattern. This is the anchor the server pins
400
+ // the checkpoint to; it is not an app-facing "seal a past
401
+ // version" control.
402
+ const version = doc.currentSeq;
403
+ if (version === null) {
404
+ throw new BrassError(
405
+ 'createCheckpoint: the document has no resolved version yet',
406
+ { status: 400 },
407
+ );
408
+ }
409
+ const branchId = doc.branchId;
410
+ const body: {
411
+ version: number;
412
+ branch_id?: string;
413
+ } = { version };
414
+ if (branchId !== null) body.branch_id = branchId;
415
+ const res = await apiFetch<{ checkpoint: DocumentCheckpoint }>({
416
+ method: 'POST',
417
+ path: `/documents/${encodeURIComponent(docId)}/checkpoints`,
418
+ body,
419
+ });
420
+ return res.checkpoint;
421
+ },
422
+ async pushToDrive(doc, input) {
423
+ const docId = doc.id;
424
+ if (docId === null) {
425
+ throw new BrassError(
426
+ 'pushToDrive requires a Document opened from the server',
427
+ { status: 400 },
428
+ );
429
+ }
430
+ const body: { message?: string } = {};
431
+ if (input?.message !== undefined) body.message = input.message;
432
+ const res = await apiFetch<PushToDriveResult>({
433
+ method: 'POST',
434
+ path: `/documents/${encodeURIComponent(docId)}/drive-sync/push`,
435
+ body,
436
+ });
437
+ return res;
438
+ },
439
+ async sourceBlob(doc) {
440
+ const docId = doc.id;
441
+ if (docId === null) {
442
+ throw new BrassError(
443
+ 'sourceBlob requires a Document opened from the server',
444
+ { status: 400 },
445
+ );
446
+ }
447
+ return apiFetch<DocumentSourceBlob>({
448
+ path: `/documents/${encodeURIComponent(docId)}/source-blob`,
449
+ });
450
+ },
451
+ claimImport(docId) {
452
+ return apiFetch<ImportClaimResponse>({
453
+ method: 'POST',
454
+ path: `/documents/${encodeURIComponent(docId)}/import-claim`,
455
+ });
456
+ },
457
+ completeImport(docId) {
458
+ return apiFetch<{ schema_ref: { app_id: string; schema_hash: string } }>({
459
+ method: 'POST',
460
+ path: `/documents/${encodeURIComponent(docId)}/import-complete`,
461
+ });
462
+ },
463
+ async getThumbnail(docId, options) {
464
+ const params = new URLSearchParams();
465
+ if (options?.version !== undefined) {
466
+ params.set('version', String(options.version));
467
+ }
468
+ if (options?.branch !== undefined) params.set('branch', options.branch);
469
+ const qs = params.toString();
470
+ const res = await apiFetch<{ thumbnail: DocumentThumbnail | null }>({
471
+ path: `/documents/${encodeURIComponent(docId)}/thumbnail${
472
+ qs ? `?${qs}` : ''
473
+ }`,
474
+ });
475
+ return res.thumbnail;
476
+ },
477
+ };
478
+ }
479
+
480
+ // Reads `SetThumbnailInput.image` into base64 plus a resolved MIME
481
+ // type. A `Blob` carries its own `type` (canvas `toBlob` sets it);
482
+ // raw `ArrayBuffer` / `Uint8Array` bytes need an explicit
483
+ // `contentType`.
484
+ async function encodeThumbnailImage(
485
+ input: SetThumbnailInput,
486
+ ): Promise<{ data: string; contentType: string }> {
487
+ let bytes: Uint8Array;
488
+ let blobType: string | undefined;
489
+ if (input.image instanceof Uint8Array) {
490
+ bytes = input.image;
491
+ } else if (input.image instanceof ArrayBuffer) {
492
+ bytes = new Uint8Array(input.image);
493
+ } else {
494
+ bytes = new Uint8Array(await input.image.arrayBuffer());
495
+ blobType = input.image.type || undefined;
496
+ }
497
+ const contentType = input.contentType ?? blobType;
498
+ if (!contentType) {
499
+ throw new BrassError(
500
+ 'setThumbnail: contentType is required when image is not a typed Blob',
501
+ { status: 400 },
502
+ );
503
+ }
504
+ return { data: bytesToBase64(bytes), contentType };
505
+ }
506
+
507
+ // Standard-alphabet base64 of a byte array. Chunked so a large image
508
+ // does not blow the argument-count limit on `String.fromCharCode`.
509
+ function bytesToBase64(bytes: Uint8Array): string {
510
+ let binary = '';
511
+ const chunk = 0x8000;
512
+ for (let i = 0; i < bytes.length; i += chunk) {
513
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
514
+ }
515
+ return btoa(binary);
516
+ }