@brass-build/client 0.4.0 → 0.5.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 (327) hide show
  1. package/AGENTS.md +142 -41
  2. package/CHANGELOG.md +93 -4
  3. package/README.md +666 -179
  4. package/dist/acting-organization.d.ts.map +1 -1
  5. package/dist/acting-organization.js +6 -11
  6. package/dist/acting-organization.js.map +1 -1
  7. package/dist/api-fetch.d.ts +8 -0
  8. package/dist/api-fetch.d.ts.map +1 -1
  9. package/dist/api-fetch.js +35 -8
  10. package/dist/api-fetch.js.map +1 -1
  11. package/dist/batched-get.d.ts +11 -0
  12. package/dist/batched-get.d.ts.map +1 -0
  13. package/dist/batched-get.js +92 -0
  14. package/dist/batched-get.js.map +1 -0
  15. package/dist/call-service.d.ts +4 -0
  16. package/dist/call-service.d.ts.map +1 -1
  17. package/dist/call-service.js +66 -3
  18. package/dist/call-service.js.map +1 -1
  19. package/dist/client.d.ts +51 -2
  20. package/dist/client.d.ts.map +1 -1
  21. package/dist/client.js +982 -124
  22. package/dist/client.js.map +1 -1
  23. package/dist/document-checkpoints.d.ts +25 -2
  24. package/dist/document-checkpoints.d.ts.map +1 -1
  25. package/dist/document-checkpoints.js +258 -61
  26. package/dist/document-checkpoints.js.map +1 -1
  27. package/dist/document-commands.d.ts.map +1 -1
  28. package/dist/document-commands.js +5 -3
  29. package/dist/document-commands.js.map +1 -1
  30. package/dist/document-contents.d.ts +10 -0
  31. package/dist/document-contents.d.ts.map +1 -1
  32. package/dist/document-contents.js +23 -1
  33. package/dist/document-contents.js.map +1 -1
  34. package/dist/document-set-subscription.d.ts +56 -0
  35. package/dist/document-set-subscription.d.ts.map +1 -0
  36. package/dist/document-set-subscription.js +240 -0
  37. package/dist/document-set-subscription.js.map +1 -0
  38. package/dist/document-signals.d.ts +6 -0
  39. package/dist/document-signals.d.ts.map +1 -1
  40. package/dist/document-signals.js +31 -1
  41. package/dist/document-signals.js.map +1 -1
  42. package/dist/document-streams.d.ts +36 -2
  43. package/dist/document-streams.d.ts.map +1 -1
  44. package/dist/document-streams.js +69 -18
  45. package/dist/document-streams.js.map +1 -1
  46. package/dist/document-thumbnail-capture.d.ts +12 -2
  47. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  48. package/dist/document-thumbnail-capture.js +103 -27
  49. package/dist/document-thumbnail-capture.js.map +1 -1
  50. package/dist/documents-namespace.d.ts +92 -10
  51. package/dist/documents-namespace.d.ts.map +1 -1
  52. package/dist/documents-namespace.js +285 -39
  53. package/dist/documents-namespace.js.map +1 -1
  54. package/dist/documents.d.ts +173 -8
  55. package/dist/documents.d.ts.map +1 -1
  56. package/dist/documents.js +1778 -310
  57. package/dist/documents.js.map +1 -1
  58. package/dist/errors.d.ts +3 -3
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js.map +1 -1
  61. package/dist/fetch-retry.d.ts +1 -0
  62. package/dist/fetch-retry.d.ts.map +1 -1
  63. package/dist/fetch-retry.js +2 -0
  64. package/dist/fetch-retry.js.map +1 -1
  65. package/dist/file-choice.d.ts +2 -0
  66. package/dist/file-choice.d.ts.map +1 -0
  67. package/dist/file-choice.js +27 -0
  68. package/dist/file-choice.js.map +1 -0
  69. package/dist/guest-link.d.ts +18 -1
  70. package/dist/guest-link.d.ts.map +1 -1
  71. package/dist/guest-link.js +329 -1
  72. package/dist/guest-link.js.map +1 -1
  73. package/dist/handle-types.d.ts +67 -5
  74. package/dist/handle-types.d.ts.map +1 -1
  75. package/dist/handles.d.ts +15 -19
  76. package/dist/handles.d.ts.map +1 -1
  77. package/dist/handles.js +231 -76
  78. package/dist/handles.js.map +1 -1
  79. package/dist/hosted-ui.d.ts +61 -0
  80. package/dist/hosted-ui.d.ts.map +1 -1
  81. package/dist/hosted-ui.js +94 -42
  82. package/dist/hosted-ui.js.map +1 -1
  83. package/dist/import-contract.d.ts +74 -0
  84. package/dist/import-contract.d.ts.map +1 -0
  85. package/dist/import-contract.js +163 -0
  86. package/dist/import-contract.js.map +1 -0
  87. package/dist/import-guest.d.ts +9 -0
  88. package/dist/import-guest.d.ts.map +1 -0
  89. package/dist/import-guest.js +155 -0
  90. package/dist/import-guest.js.map +1 -0
  91. package/dist/import-report.d.ts +17 -0
  92. package/dist/import-report.d.ts.map +1 -0
  93. package/dist/import-report.js +7 -0
  94. package/dist/import-report.js.map +1 -0
  95. package/dist/importer-wait.d.ts +2 -0
  96. package/dist/importer-wait.d.ts.map +1 -0
  97. package/dist/importer-wait.js +5 -0
  98. package/dist/importer-wait.js.map +1 -0
  99. package/dist/importer.d.ts +18 -4
  100. package/dist/importer.d.ts.map +1 -1
  101. package/dist/importer.js +322 -105
  102. package/dist/importer.js.map +1 -1
  103. package/dist/in-development.d.ts +2 -0
  104. package/dist/in-development.d.ts.map +1 -0
  105. package/dist/in-development.js +10 -0
  106. package/dist/in-development.js.map +1 -0
  107. package/dist/index.d.ts +16 -8
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +24 -8
  110. package/dist/index.js.map +1 -1
  111. package/dist/init-keys.d.ts +1 -1
  112. package/dist/init-keys.d.ts.map +1 -1
  113. package/dist/init-keys.js +6 -1
  114. package/dist/init-keys.js.map +1 -1
  115. package/dist/input-documents.d.ts +67 -0
  116. package/dist/input-documents.d.ts.map +1 -0
  117. package/dist/input-documents.js +693 -0
  118. package/dist/input-documents.js.map +1 -0
  119. package/dist/inputs-status.d.ts +80 -0
  120. package/dist/inputs-status.d.ts.map +1 -0
  121. package/dist/inputs-status.js +460 -0
  122. package/dist/inputs-status.js.map +1 -0
  123. package/dist/inputs.d.ts +2 -0
  124. package/dist/inputs.d.ts.map +1 -0
  125. package/dist/inputs.js +793 -0
  126. package/dist/inputs.js.map +1 -0
  127. package/dist/internal.d.ts +6 -1
  128. package/dist/internal.d.ts.map +1 -1
  129. package/dist/internal.js +4 -1
  130. package/dist/internal.js.map +1 -1
  131. package/dist/join-content.d.ts +33 -0
  132. package/dist/join-content.d.ts.map +1 -0
  133. package/dist/join-content.js +114 -0
  134. package/dist/join-content.js.map +1 -0
  135. package/dist/join-stamp.d.ts +3 -0
  136. package/dist/join-stamp.d.ts.map +1 -0
  137. package/dist/join-stamp.js +12 -0
  138. package/dist/join-stamp.js.map +1 -0
  139. package/dist/manifest-drift.d.ts +9 -0
  140. package/dist/manifest-drift.d.ts.map +1 -0
  141. package/dist/manifest-drift.js +46 -0
  142. package/dist/manifest-drift.js.map +1 -0
  143. package/dist/open-document.d.ts +109 -2
  144. package/dist/open-document.d.ts.map +1 -1
  145. package/dist/open-document.js +1822 -811
  146. package/dist/open-document.js.map +1 -1
  147. package/dist/page-restore.d.ts +12 -0
  148. package/dist/page-restore.d.ts.map +1 -0
  149. package/dist/page-restore.js +23 -0
  150. package/dist/page-restore.js.map +1 -0
  151. package/dist/placement.d.ts +13 -0
  152. package/dist/placement.d.ts.map +1 -0
  153. package/dist/placement.js +34 -0
  154. package/dist/placement.js.map +1 -0
  155. package/dist/presence.d.ts +1 -0
  156. package/dist/presence.d.ts.map +1 -1
  157. package/dist/presence.js +16 -4
  158. package/dist/presence.js.map +1 -1
  159. package/dist/read-only-document.d.ts +87 -0
  160. package/dist/read-only-document.d.ts.map +1 -0
  161. package/dist/read-only-document.js +4 -0
  162. package/dist/read-only-document.js.map +1 -0
  163. package/dist/record-paging.d.ts +63 -6
  164. package/dist/record-paging.d.ts.map +1 -1
  165. package/dist/record-paging.js +122 -29
  166. package/dist/record-paging.js.map +1 -1
  167. package/dist/references.d.ts +40 -0
  168. package/dist/references.d.ts.map +1 -0
  169. package/dist/references.js +135 -0
  170. package/dist/references.js.map +1 -0
  171. package/dist/request-lane.d.ts +5 -0
  172. package/dist/request-lane.d.ts.map +1 -0
  173. package/dist/request-lane.js +48 -0
  174. package/dist/request-lane.js.map +1 -0
  175. package/dist/sealed-arrow.d.ts +170 -0
  176. package/dist/sealed-arrow.d.ts.map +1 -0
  177. package/dist/sealed-arrow.js +769 -0
  178. package/dist/sealed-arrow.js.map +1 -0
  179. package/dist/sealed-cache.d.ts +21 -0
  180. package/dist/sealed-cache.d.ts.map +1 -0
  181. package/dist/sealed-cache.js +247 -0
  182. package/dist/sealed-cache.js.map +1 -0
  183. package/dist/sealed-encode.d.ts +87 -0
  184. package/dist/sealed-encode.d.ts.map +1 -0
  185. package/dist/sealed-encode.js +876 -0
  186. package/dist/sealed-encode.js.map +1 -0
  187. package/dist/sealed-handles.d.ts +45 -0
  188. package/dist/sealed-handles.d.ts.map +1 -0
  189. package/dist/sealed-handles.js +408 -0
  190. package/dist/sealed-handles.js.map +1 -0
  191. package/dist/sealed-store.d.ts +242 -0
  192. package/dist/sealed-store.d.ts.map +1 -0
  193. package/dist/sealed-store.js +1155 -0
  194. package/dist/sealed-store.js.map +1 -0
  195. package/dist/serializer.d.ts +6 -15
  196. package/dist/serializer.d.ts.map +1 -1
  197. package/dist/session-toolbar.d.ts +20 -5
  198. package/dist/session-toolbar.d.ts.map +1 -1
  199. package/dist/session-toolbar.js +419 -67
  200. package/dist/session-toolbar.js.map +1 -1
  201. package/dist/session.d.ts +38 -1
  202. package/dist/session.d.ts.map +1 -1
  203. package/dist/session.js +562 -79
  204. package/dist/session.js.map +1 -1
  205. package/dist/sha256.d.ts +3 -0
  206. package/dist/sha256.d.ts.map +1 -0
  207. package/dist/sha256.js +74 -0
  208. package/dist/sha256.js.map +1 -0
  209. package/dist/stable-handles.d.ts +19 -0
  210. package/dist/stable-handles.d.ts.map +1 -0
  211. package/dist/stable-handles.js +193 -0
  212. package/dist/stable-handles.js.map +1 -0
  213. package/dist/staged-seal.d.ts +2 -0
  214. package/dist/staged-seal.d.ts.map +1 -0
  215. package/dist/staged-seal.js +76 -0
  216. package/dist/staged-seal.js.map +1 -0
  217. package/dist/subscriber-delivery.d.ts +8 -0
  218. package/dist/subscriber-delivery.d.ts.map +1 -0
  219. package/dist/subscriber-delivery.js +31 -0
  220. package/dist/subscriber-delivery.js.map +1 -0
  221. package/dist/tail-op-import.d.ts +10 -0
  222. package/dist/tail-op-import.d.ts.map +1 -0
  223. package/dist/tail-op-import.js +33 -0
  224. package/dist/tail-op-import.js.map +1 -0
  225. package/dist/topic-lifecycle.d.ts +6 -0
  226. package/dist/topic-lifecycle.d.ts.map +1 -1
  227. package/dist/topic-lifecycle.js +32 -2
  228. package/dist/topic-lifecycle.js.map +1 -1
  229. package/dist/trial-replace-dialog.d.ts +10 -0
  230. package/dist/trial-replace-dialog.d.ts.map +1 -0
  231. package/dist/trial-replace-dialog.js +153 -0
  232. package/dist/trial-replace-dialog.js.map +1 -0
  233. package/dist/types.d.ts +8 -1
  234. package/dist/types.d.ts.map +1 -1
  235. package/dist/undo.d.ts +60 -5
  236. package/dist/undo.d.ts.map +1 -1
  237. package/dist/undo.js +344 -75
  238. package/dist/undo.js.map +1 -1
  239. package/dist/version-bar.d.ts +1 -0
  240. package/dist/version-bar.d.ts.map +1 -1
  241. package/dist/version-bar.js +2 -1
  242. package/dist/version-bar.js.map +1 -1
  243. package/dist/version.d.ts +2 -2
  244. package/dist/version.js +1 -1
  245. package/dist/ws-client.d.ts +21 -0
  246. package/dist/ws-client.d.ts.map +1 -1
  247. package/dist/ws-client.js +192 -8
  248. package/dist/ws-client.js.map +1 -1
  249. package/dist/ws-frame-limit.d.ts +1 -0
  250. package/dist/ws-frame-limit.d.ts.map +1 -1
  251. package/dist/ws-frame-limit.js.map +1 -1
  252. package/docs/app-manifest.md +121 -17
  253. package/docs/custom-chrome.md +24 -16
  254. package/docs/hosted-ui-advanced.md +38 -6
  255. package/docs/imported-documents.md +104 -51
  256. package/docs/presence.md +16 -4
  257. package/docs/schemas.md +8 -5
  258. package/docs/services.md +6 -6
  259. package/docs/streams.md +139 -150
  260. package/docs/text-fields.md +26 -19
  261. package/package.json +5 -2
  262. package/src/acting-organization.ts +6 -12
  263. package/src/api-fetch.ts +53 -3
  264. package/src/auth-flow.ts +1 -1
  265. package/src/batched-get.ts +117 -0
  266. package/src/call-service.ts +78 -3
  267. package/src/client.ts +1092 -121
  268. package/src/document-checkpoints.ts +307 -65
  269. package/src/document-commands.ts +5 -5
  270. package/src/document-contents.ts +27 -1
  271. package/src/document-set-subscription.ts +274 -0
  272. package/src/document-signals.ts +47 -5
  273. package/src/document-streams.ts +85 -16
  274. package/src/document-thumbnail-capture.ts +118 -32
  275. package/src/documents-namespace.ts +539 -84
  276. package/src/documents.ts +2017 -348
  277. package/src/errors.ts +17 -6
  278. package/src/fetch-retry.ts +5 -0
  279. package/src/file-choice.ts +26 -0
  280. package/src/guest-link.ts +362 -1
  281. package/src/handle-types.ts +73 -47
  282. package/src/handles.ts +253 -83
  283. package/src/hosted-ui.ts +211 -50
  284. package/src/import-contract.ts +227 -0
  285. package/src/import-guest.ts +193 -0
  286. package/src/import-report.ts +24 -0
  287. package/src/importer-wait.ts +4 -0
  288. package/src/importer.ts +386 -131
  289. package/src/in-development.ts +10 -0
  290. package/src/index.ts +76 -16
  291. package/src/init-keys.ts +6 -1
  292. package/src/input-documents.ts +819 -0
  293. package/src/inputs-status.ts +598 -0
  294. package/src/inputs.ts +1090 -0
  295. package/src/internal.ts +7 -0
  296. package/src/join-content.ts +135 -0
  297. package/src/join-stamp.ts +12 -0
  298. package/src/manifest-drift.ts +54 -0
  299. package/src/open-document.ts +2346 -930
  300. package/src/page-restore.ts +38 -0
  301. package/src/placement.ts +54 -0
  302. package/src/presence.ts +21 -5
  303. package/src/read-only-document.ts +133 -0
  304. package/src/record-paging.ts +147 -25
  305. package/src/references.ts +207 -0
  306. package/src/request-lane.ts +59 -0
  307. package/src/sealed-arrow.ts +843 -0
  308. package/src/sealed-cache.ts +267 -0
  309. package/src/sealed-encode.ts +1015 -0
  310. package/src/sealed-handles.ts +486 -0
  311. package/src/sealed-store.ts +1330 -0
  312. package/src/serializer.ts +8 -20
  313. package/src/session-toolbar.ts +463 -72
  314. package/src/session.ts +740 -99
  315. package/src/sha256.ts +65 -0
  316. package/src/stable-handles.ts +235 -0
  317. package/src/staged-seal.ts +90 -0
  318. package/src/subscriber-delivery.ts +37 -0
  319. package/src/tail-op-import.ts +30 -0
  320. package/src/topic-lifecycle.ts +38 -2
  321. package/src/trial-replace-dialog.ts +163 -0
  322. package/src/types.ts +6 -6
  323. package/src/undo.ts +386 -73
  324. package/src/version-bar.ts +5 -1
  325. package/src/version.ts +1 -1
  326. package/src/ws-client.ts +208 -16
  327. package/src/ws-frame-limit.ts +1 -0
package/src/hosted-ui.ts CHANGED
@@ -101,6 +101,98 @@ export interface SelectDocumentResult {
101
101
  branch_id?: string;
102
102
  }
103
103
 
104
+ /** What an app passes to `Brass.chooseInputs`. */
105
+ export interface ChooseInputsOptions {
106
+ /** The references this app already holds. The picker opens with them
107
+ * ticked and resolves with the whole set, so replace what you kept with
108
+ * the result rather than appending to it. */
109
+ selected?: readonly InputReference[];
110
+ /** Narrows the files of those families by name or extension. */
111
+ accept?: SelectionNarrowing;
112
+ /** One line naming what the app is asking for. */
113
+ prompt?: string;
114
+ }
115
+
116
+ /** What an app passes to `Brass.chooseInput`. */
117
+ export interface ChooseInputOptions {
118
+ /** The reference this app already holds for this choice. The picker opens
119
+ * with that file marked. */
120
+ selected?: InputReference | null;
121
+ /** Narrows the files of those families by name or extension. */
122
+ accept?: SelectionNarrowing;
123
+ /** One line naming what the app is asking for. */
124
+ prompt?: string;
125
+ }
126
+
127
+ /**
128
+ * One file, folder, project or organization a person chose for this app,
129
+ * which `doc.dependsOn` records as a reference. A place (`organization`,
130
+ * `project`, or a folder named by `doc_id`) keeps answering, so a document
131
+ * filed there later is included; a file is that one document.
132
+ */
133
+ export interface InputReference {
134
+ organization_id: string;
135
+ /** `document` names a file or a folder by `doc_id`. */
136
+ extent: 'organization' | 'project' | 'document';
137
+ project_id?: string;
138
+ doc_id?: string;
139
+ /** The name the picker showed for this entry when the reader chose it. It
140
+ * does not follow a later rename; choosing again returns the new name. */
141
+ label?: string;
142
+ /** What the picker a folder, project or organization was chosen in
143
+ * offered. Listing the reference includes only the documents it admits, so
144
+ * a place chosen in two pickers stays apart. Keep it with the reference. */
145
+ accept?: SelectionAccept;
146
+ }
147
+
148
+ /** The families a picker offers, as your manifest's `opens` and `inputs`
149
+ * declare them. A family your manifest does not declare offers nothing. */
150
+ export type DocumentFamilies = readonly string[];
151
+
152
+ /** Narrows the files of the families a request offers. A file passes when
153
+ * it matches a name or an extension given. */
154
+ export interface SelectionNarrowing {
155
+ /** Names to match, case-insensitively, against a document's name and the
156
+ * name of the file it was imported from. */
157
+ names?: readonly string[];
158
+ /** File extensions, with or without the leading dot. */
159
+ extensions?: readonly string[];
160
+ }
161
+
162
+ /** What a picker offered: the families it named, and any narrowing. */
163
+ export interface SelectionAccept extends SelectionNarrowing {
164
+ /** Absent when the picker offered every family. */
165
+ families?: readonly string[];
166
+ }
167
+
168
+ // The `accept` a picker sends, from the families and narrowing an app passed.
169
+ // Throws when the families are missing, since without them the picker would
170
+ // offer everything the app can read, its own documents included.
171
+ function pickerAccept(
172
+ method: string,
173
+ families: unknown,
174
+ narrowing: SelectionNarrowing | undefined,
175
+ ): SelectionAccept | undefined {
176
+ const named =
177
+ Array.isArray(families) &&
178
+ families.length > 0 &&
179
+ families.every((t) => typeof t === 'string' && t.trim() !== '');
180
+ if (families !== '*' && !named) {
181
+ throw new Error(
182
+ `${method}: pass the families to offer first, such as ['ifc']`,
183
+ );
184
+ }
185
+ if (narrowing !== undefined && ('families' in narrowing || 'types' in narrowing)) {
186
+ throw new Error(`${method}: name the families in the first argument rather than in accept`);
187
+ }
188
+ const accept: SelectionAccept = {
189
+ ...(named ? { families: [...(families as readonly string[])] } : {}),
190
+ ...(narrowing?.names?.length ? { names: [...narrowing.names] } : {}),
191
+ ...(narrowing?.extensions?.length ? { extensions: [...narrowing.extensions] } : {}),
192
+ };
193
+ return Object.keys(accept).length > 0 ? accept : undefined;
194
+ }
195
+
104
196
  // Result of the hosted create-destination dialog: where a new document goes,
105
197
  // plus the chosen name. The dialog resolves a destination only; the SDK does
106
198
  // the create itself so the app's credential path stamps `creating_app_id` and
@@ -109,6 +201,7 @@ export interface SelectDocumentResult {
109
201
  export interface CreateDestinationResult {
110
202
  organization_id: string;
111
203
  project_id: string | null;
204
+ folder_id?: string | null;
112
205
  name: string;
113
206
  }
114
207
 
@@ -177,9 +270,9 @@ export interface ServiceCredentialResult {
177
270
  // how a `behind` / `diverged` drive-linked doc was resolved. `null` when
178
271
  // dismissed.
179
272
  export interface DriveResolveResult {
180
- // The fresh mirror doc that took over the link (take-source / keep-both
181
- // re-import the remote into a new doc); `null` when publish-over resolved
182
- // in place.
273
+ // A document to switch onto, or `null` when the resolution kept this one:
274
+ // publish-over, take-source and keep-both all resolve in place, and an open
275
+ // copy moves onto the drive's version when it lands.
183
276
  newDocId: string | null;
184
277
  }
185
278
 
@@ -208,6 +301,23 @@ export interface HostedUiLauncher {
208
301
  // Iframe-only with a `BrassError` reject (a returned value can't survive a
209
302
  // redirect fallback).
210
303
  selectDocument(): Promise<SelectDocumentResult | null>;
304
+ // The same picker as a tree of tick boxes, asking which documents this
305
+ // app has rather than which one to open now. Ticking a folder includes
306
+ // what is filed there later; ticking a document is that document. What
307
+ // they choose is their own reach, so every later read is still clamped to
308
+ // what they can see. Resolves with everything currently chosen.
309
+ // Iframe-only, same rationale as `selectDocument`.
310
+ chooseInputs(
311
+ families: DocumentFamilies,
312
+ options?: ChooseInputsOptions,
313
+ ): Promise<InputReference[] | null>;
314
+ // The same picker as the ordinary Open list, where one press on a file
315
+ // chooses it. Resolves with that file's reference, or `null` when the
316
+ // reader closes it.
317
+ chooseInput(
318
+ families: DocumentFamilies,
319
+ options?: ChooseInputOptions,
320
+ ): Promise<InputReference | null>;
211
321
  // The document picker opened straight on its New panel to ask where a new
212
322
  // document goes. Used by `Brass.session.create()` when a cold create is
213
323
  // ambiguous across multiple orgs, so the user picks rather than the SDK
@@ -271,10 +381,9 @@ export interface HostedUiLauncher {
271
381
  // (default selection is the only org, or the first by membership time).
272
382
  installApp(organizationId?: string): Promise<InstallAppResult | null>;
273
383
  // Hosted drive-divergence resolution (from `Document.resolveDrive()`):
274
- // publish over the source, take the source, or keep both. Take-source /
275
- // keep-both mint a fresh mirror doc (its id rides the result for the app to
276
- // switch onto); publish-over resolves in place (`newDocId: null`). Resolves
277
- // with the outcome or `null`. Iframe-only with a `BrassError` reject.
384
+ // publish over the source, take the source, or keep both, each in place
385
+ // (`newDocId: null`). Resolves with the outcome or `null`. Iframe-only with
386
+ // a `BrassError` reject.
278
387
  driveResolve(docId: string): Promise<DriveResolveResult | null>;
279
388
  // Hosted surface for a service credential this app is waiting on. Whoever
280
389
  // can supply it connects the account and admits this app to it; anyone else
@@ -296,10 +405,10 @@ export interface HostedUiLauncherOptions {
296
405
  // list; absent leaves it on everything the caller can reach, which is what
297
406
  // an app that has not resolved one yet should show.
298
407
  actingOrganizationId?: () => string | null;
299
- // Threaded into param payloads for surfaces that need it (`installApp`).
300
- // Every Brass app shares one sign-in client, so the JWT can't
301
- // disambiguate the asking app; the embed page reads `appId` off the bridge.
302
- // Optional so non-install fixtures can omit it.
408
+ // Sent as `appId` in every surface's init. Every Brass app shares one
409
+ // sign-in client, so the JWT can't name the asking app; the embed page reads
410
+ // it off the bridge, and checks the framing page may host that app before it
411
+ // renders. Optional so fixtures can omit it.
303
412
  callingAppId?: string;
304
413
  // Pins the modal + embed page to a theme. Defaults to `'light'`; pass
305
414
  // `'auto'` to follow OS preference. Host apps that force a theme pass it so
@@ -941,16 +1050,16 @@ export function createHostedUiLauncher(
941
1050
  // would put a dark embed inside a light host on a dark-OS user.
942
1051
  const theme: HostedUiTheme = opts.theme ?? 'light';
943
1052
 
944
- // Name the user this app is acting as in every surface's bridge params.
945
- // Read at open time, not launcher-construction time, since the app may
946
- // authenticate after the launcher is built. Every embed surface checks the
947
- // same way, so this is cross-cutting rather than per-surface like `appId`.
948
- const withActingSub = (
949
- params: Record<string, unknown>,
950
- ): Record<string, unknown> => {
1053
+ // Name the app and the user it acts as in every surface's bridge params.
1054
+ // The app id is what the embed checks the framing page against, so a
1055
+ // surface opened without it is one whose host goes unchecked. The acting sub
1056
+ // is read at open time, not launcher-construction time, since the app may
1057
+ // authenticate after the launcher is built.
1058
+ const withCaller = (params: Record<string, unknown>): Record<string, unknown> => {
951
1059
  const actingSub = opts.getActingSub?.();
952
1060
  return {
953
1061
  ...params,
1062
+ ...(opts.callingAppId !== undefined ? { appId: opts.callingAppId } : {}),
954
1063
  ...(actingSub ? { actingSub } : {}),
955
1064
  };
956
1065
  };
@@ -959,17 +1068,17 @@ export function createHostedUiLauncher(
959
1068
  // dismissal, otherwise the payload. `exportDoc` threads the export
960
1069
  // serializer for the document surfaces whose page offers a Download
961
1070
  // action.
962
- const launch = async <T>(
1071
+ const openSurface = (
963
1072
  surface: string,
964
1073
  params: Record<string, unknown>,
965
1074
  unavailable: UnavailableMode,
966
1075
  exportDoc?: boolean,
967
- ): Promise<T | null> => {
968
- const outcome = await openEmbed(
1076
+ ): Promise<EmbedOutcome | null> =>
1077
+ openEmbed(
969
1078
  baseUrl,
970
1079
  dashboardOrigin,
971
1080
  surface,
972
- withActingSub(params),
1081
+ withCaller(params),
973
1082
  unavailable,
974
1083
  theme,
975
1084
  exportDoc ? opts.serializeForExport : undefined,
@@ -978,25 +1087,69 @@ export function createHostedUiLauncher(
978
1087
  opts.onLocalDelete,
979
1088
  opts.onSignInRequested,
980
1089
  );
1090
+
1091
+ const launch = async <T>(
1092
+ surface: string,
1093
+ params: Record<string, unknown>,
1094
+ unavailable: UnavailableMode,
1095
+ exportDoc?: boolean,
1096
+ ): Promise<T | null> => {
1097
+ const outcome = await openSurface(surface, params, unavailable, exportDoc);
981
1098
  if (!outcome) return null;
982
1099
  return outcome.payload as T;
983
1100
  };
984
1101
 
985
- // Every Brass app shares one sign-in client, so a request's JWT
986
- // does not say which app made it. Surfaces that must name the caller
987
- // (its scopes on the install screen, its documents in the picker, the app
988
- // a share link should open in) read this off the bridge instead.
989
- const callerAppParam = (): Record<string, string> =>
990
- opts.callingAppId !== undefined ? { appId: opts.callingAppId } : {};
1102
+ // One input picker at a time. A second call while one is open (a double
1103
+ // press) shares it when it asks the same question, so the reader answers
1104
+ // once and every caller hears that answer; a different question is
1105
+ // refused rather than stacked behind the first. The slot clears on a
1106
+ // reaction registered on the dialog's own promise, ahead of any caller's,
1107
+ // so a call made as soon as the first settles opens a fresh picker.
1108
+ let inputPicker: { key: string; promise: Promise<InputReference[] | null> } | null = null;
1109
+ const pickInputs = (params: Record<string, unknown>): Promise<InputReference[] | null> => {
1110
+ const key = JSON.stringify(params);
1111
+ if (inputPicker) {
1112
+ if (inputPicker.key === key) return inputPicker.promise;
1113
+ return Promise.reject(new BrassError('A document picker is already open.'));
1114
+ }
1115
+ const opened = openSurface('select-document', params, 'reject');
1116
+ const promise = opened.then((outcome) =>
1117
+ outcome ? (outcome.payload as InputReference[]) : null,
1118
+ );
1119
+ const entry = { key, promise };
1120
+ const clear = (): void => {
1121
+ if (inputPicker === entry) inputPicker = null;
1122
+ };
1123
+ opened.then(clear, clear);
1124
+ inputPicker = entry;
1125
+ return promise;
1126
+ };
1127
+
1128
+ // The params a choice opens the picker with. `choose` is what the picker
1129
+ // reads to answer with references; everything else about the surface is
1130
+ // the same, which is why this is one param rather than a second surface.
1131
+ // A choice takes no redirect: the fallback carries its answer home in
1132
+ // one-shot URL params built for an opened document, so a browser that
1133
+ // cannot host the frame rejects instead of ejecting the reader.
1134
+ const chooseParams = (
1135
+ method: string,
1136
+ families: DocumentFamilies,
1137
+ options: { accept?: SelectionNarrowing; prompt?: string } | undefined,
1138
+ selected: readonly InputReference[],
1139
+ ): Record<string, unknown> => {
1140
+ const accept = pickerAccept(method, families, options?.accept);
1141
+ const params: Record<string, unknown> = { choose: true };
1142
+ if (selected.length) params['selected'] = [...selected];
1143
+ if (accept) params['accept'] = accept;
1144
+ if (options?.prompt !== undefined) params['prompt'] = options.prompt;
1145
+ const actingOrganizationId = opts.actingOrganizationId?.();
1146
+ if (actingOrganizationId) params['organizationId'] = actingOrganizationId;
1147
+ return params;
1148
+ };
991
1149
 
992
1150
  return {
993
1151
  async selectDocument(): Promise<SelectDocumentResult | null> {
994
- // Thread the calling app id over the bridge (every Brass app shares one
995
- // sign-in client, so the origin can't otherwise tell which
996
- // app opened the picker). The embed page uses it for a "only documents
997
- // this app can open" filter.
998
1152
  const params: Record<string, unknown> = {};
999
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1000
1153
  // Where the app is working, so the picker opens there rather than on
1001
1154
  // every document the person can reach. Without it the first list an app
1002
1155
  // shows spans organizations the app is not installed on, and picking
@@ -1027,6 +1180,26 @@ export function createHostedUiLauncher(
1027
1180
  pickReturn ? 'redirect-with-value' : 'reject',
1028
1181
  );
1029
1182
  },
1183
+ async chooseInputs(
1184
+ families: DocumentFamilies,
1185
+ options?: ChooseInputsOptions,
1186
+ ): Promise<InputReference[] | null> {
1187
+ return pickInputs(
1188
+ chooseParams('chooseInputs', families, options, options?.selected ?? []),
1189
+ );
1190
+ },
1191
+ async chooseInput(
1192
+ families: DocumentFamilies,
1193
+ options?: ChooseInputOptions,
1194
+ ): Promise<InputReference | null> {
1195
+ const held = options?.selected ?? null;
1196
+ const params = chooseParams('chooseInput', families, options, held ? [held] : []);
1197
+ // `multiple: false` draws the browsing list rather than the tree, and
1198
+ // the picker answers with an array of the one reference.
1199
+ params['multiple'] = false;
1200
+ const chosen = await pickInputs(params);
1201
+ return chosen?.[0] ?? null;
1202
+ },
1030
1203
  async selectCreateDestination(options?: {
1031
1204
  name?: string;
1032
1205
  }): Promise<CreateDestinationResult | null> {
@@ -1035,13 +1208,11 @@ export function createHostedUiLauncher(
1035
1208
  // it back instead of creating the doc, so the create stays on the SDK's
1036
1209
  // credential path. `name` pre-fills the dialog.
1037
1210
  const params: Record<string, unknown> = { intent: 'create' };
1038
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1039
1211
  if (options?.name !== undefined) params['name'] = options.name;
1040
1212
  return launch<CreateDestinationResult>('select-document', params, 'reject');
1041
1213
  },
1042
1214
  async acquireOrganization(): Promise<AcquireOrganizationResult | null> {
1043
1215
  const params: Record<string, unknown> = {};
1044
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1045
1216
  return launch<AcquireOrganizationResult>(
1046
1217
  'acquire-organization',
1047
1218
  params,
@@ -1055,7 +1226,7 @@ export function createHostedUiLauncher(
1055
1226
  // `newDocId` into `openSiblingDoc`, and this one cannot.
1056
1227
  await launch(
1057
1228
  'share-document',
1058
- { ...callerAppParam(), docId, focus: 'share' },
1229
+ { docId, focus: 'share' },
1059
1230
  'redirect',
1060
1231
  true,
1061
1232
  );
@@ -1079,7 +1250,7 @@ export function createHostedUiLauncher(
1079
1250
  // `newDocId` into `openSiblingDoc`. The hosted page is served
1080
1251
  // fresh against pinned SDKs, so it offers the Open only to a host
1081
1252
  // that says it will honour it.
1082
- { ...callerAppParam(), docId, focus, canOpenCopy: true },
1253
+ { docId, focus, canOpenCopy: true },
1083
1254
  'redirect',
1084
1255
  true,
1085
1256
  );
@@ -1104,23 +1275,17 @@ export function createHostedUiLauncher(
1104
1275
  currentOrganizationId?: string,
1105
1276
  ): Promise<SelectOrganizationResult | null> {
1106
1277
  // The list is read on the dashboard origin and never crosses the
1107
- // bridge; only the chosen organization comes back. `appId` rides it for
1108
- // the same reason the install dialog needs it: one sign-in client
1109
- // serves every app, so the JWT cannot say which app is asking.
1278
+ // bridge; only the chosen organization comes back.
1110
1279
  const params: Record<string, unknown> = {};
1111
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1112
1280
  if (currentOrganizationId !== undefined) {
1113
1281
  params['organizationId'] = currentOrganizationId;
1114
1282
  }
1115
1283
  return launch<SelectOrganizationResult>('select-organization', params, 'reject');
1116
1284
  },
1117
1285
  async installApp(organizationId?: string): Promise<InstallAppResult | null> {
1118
- // Thread the caller's `appId` over the bridge so the embed page renders
1119
- // the right app's name + scopes (every Brass app shares one sign-in
1120
- // user-pool client, so the JWT can't disambiguate). On confirm the
1121
- // embed page POSTs the install server-side and forwards the response.
1286
+ // The embed page renders the app the init names, with its scopes. On
1287
+ // confirm it POSTs the install server-side and forwards the response.
1122
1288
  const params: Record<string, unknown> = {};
1123
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1124
1289
  if (organizationId !== undefined) params['organizationId'] = organizationId;
1125
1290
  return launch<InstallAppResult>('install-app', params, 'reject');
1126
1291
  },
@@ -1131,11 +1296,7 @@ export function createHostedUiLauncher(
1131
1296
  organizationId: string,
1132
1297
  host: string,
1133
1298
  ): Promise<ServiceCredentialResult | null> {
1134
- // `appId` rides the bridge for the same reason the install dialog needs
1135
- // it: one sign-in client serves every app, so the JWT cannot say which
1136
- // app is asking, and this screen grants that app in particular.
1137
1299
  const params: Record<string, unknown> = { organizationId, host };
1138
- if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
1139
1300
  return launch<ServiceCredentialResult>('service-credential', params, 'reject');
1140
1301
  },
1141
1302
  };
@@ -0,0 +1,227 @@
1
+ // The importer contract, shared by every place an importer runs.
2
+ //
3
+ // An importer is a leaf: given one file, it returns content as JSON covering
4
+ // one or more named streams of one document, plus an optional report. It
5
+ // holds no Document and calls nothing on Brass. Whatever runs it (the SDK in a
6
+ // browser tab, the platform's import worker) reads the file, encodes what it returns
7
+ // with `encodeImportStreams`, and hands the encoded streams to the platform,
8
+ // which seals them as the document's first content. Nothing here or in a
9
+ // runner knows which importer it is running.
10
+ //
11
+ // Loro-free, so the platform's worker and the published SDK share it.
12
+
13
+ import type { ImportReport } from './import-report.js';
14
+ import { sealedRootKindOf, type SealedStoreBuiltBy } from './sealed-store.js';
15
+
16
+ /** The file an importer is given. */
17
+ export interface ImportFile {
18
+ /** The file's own name, extension included. */
19
+ name: string;
20
+ /** The file's media type, as it was uploaded. */
21
+ content_type: string;
22
+ /** The file's size in bytes. */
23
+ byte_size: number;
24
+ /** @internal A short-lived URL the bytes can be fetched from, where the
25
+ * runner has one. */
26
+ url?: string;
27
+ /** The file's bytes. */
28
+ bytes(): Promise<Uint8Array>;
29
+ }
30
+
31
+ /**
32
+ * One stream's content as JSON: one entry per top-level collection, the same
33
+ * keys `doc.map`, `doc.list`, `doc.orderedMap` and `doc.text` read it back
34
+ * through. An object is a map (its entries are the records), an array is a
35
+ * list, and a string is a text. An ordered map is its two collections: the
36
+ * map of records and the list of their keys.
37
+ */
38
+ export type ImportContent = Record<string, Record<string, unknown> | readonly unknown[] | string>;
39
+
40
+ /** What an importer returns: the content of every stream it produces, keyed
41
+ * by stream name, and what it skipped or assumed. The document's own stream
42
+ * is `default`; any other name is a stream an app opens with
43
+ * `doc.stream(name)`. */
44
+ export interface ImportResult {
45
+ streams: Record<string, ImportContent>;
46
+ /** What the import skipped or assumed. Omit it for a file read cleanly. */
47
+ report?: ImportReport;
48
+ }
49
+
50
+ export type DocumentImporter = (
51
+ file: ImportFile,
52
+ ) => ImportResult | Promise<ImportResult>;
53
+
54
+ /** The document's own stream. */
55
+ export const DEFAULT_IMPORT_STREAM = 'default';
56
+
57
+ // A stream name is a manifest name token (`NAME_TOKEN_PATTERN` in the
58
+ // platform's contracts): the platform refuses anything else.
59
+ const STREAM_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
60
+
61
+ /** A result an importer returned that no runner can store, naming what is
62
+ * wrong with it. */
63
+ export class ImportResultError extends Error {}
64
+
65
+ /**
66
+ * The importer's return value, validated, or null when it returned no
67
+ * content. Throws `ImportResultError` naming the stream or collection it
68
+ * could not read.
69
+ */
70
+ export function readImportResult(value: unknown): ImportResult | null {
71
+ if (value === undefined || value === null) return null;
72
+ if (typeof value !== 'object' || Array.isArray(value)) {
73
+ throw new ImportResultError('The importer must return { streams }.');
74
+ }
75
+ const { streams, report } = value as { streams?: unknown; report?: unknown };
76
+ if (streams === undefined || streams === null) return null;
77
+ if (typeof streams !== 'object' || Array.isArray(streams)) {
78
+ throw new ImportResultError(
79
+ 'The importer returned streams that are not an object keyed by stream name.',
80
+ );
81
+ }
82
+ for (const [name, content] of Object.entries(streams)) {
83
+ if (!STREAM_NAME.test(name)) {
84
+ throw new ImportResultError(
85
+ `The importer returned a stream named "${name}". A stream name is ` +
86
+ 'lowercase letters, digits, ".", "-" and "_".',
87
+ );
88
+ }
89
+ if (content === null || typeof content !== 'object' || Array.isArray(content)) {
90
+ throw new ImportResultError(
91
+ `The importer returned stream "${name}" as something other than an ` +
92
+ 'object of collections.',
93
+ );
94
+ }
95
+ for (const [key, root] of Object.entries(content as Record<string, unknown>)) {
96
+ if (sealedRootKindOf(root) === null) {
97
+ throw new ImportResultError(
98
+ `The importer returned "${key}" in stream "${name}" as neither an ` +
99
+ 'object, an array, nor a string.',
100
+ );
101
+ }
102
+ }
103
+ }
104
+ return {
105
+ streams: streams as Record<string, ImportContent>,
106
+ ...(report !== undefined && report !== null
107
+ ? { report: report as ImportReport }
108
+ : {}),
109
+ };
110
+ }
111
+
112
+ /** How many entries a stream's content holds, across its collections. */
113
+ export function importContentEntries(content: ImportContent): number {
114
+ let count = 0;
115
+ for (const root of Object.values(content)) {
116
+ if (typeof root === 'string') count += root === '' ? 0 : 1;
117
+ else if (Array.isArray(root)) count += root.length;
118
+ else count += Object.keys(root).length;
119
+ }
120
+ return count;
121
+ }
122
+
123
+ /** One stream, encoded in the form its producer uploads
124
+ * (`encodeSealedUpload`). */
125
+ export interface EncodedImportStream {
126
+ name: string;
127
+ bytes: Uint8Array;
128
+ /** Entries the stream holds, counted before the encode consumed them. */
129
+ entries: number;
130
+ }
131
+
132
+ /**
133
+ * Encode every stream a result holds, the default stream first. A stream
134
+ * holding no entries is left out, and a result where every stream is empty
135
+ * encodes to nothing, which a runner reports rather than seals: the import
136
+ * stamp is permanent, and a document stamped with nothing in it has no path
137
+ * back to its file.
138
+ *
139
+ * CONSUMES the result's map values (see `sealedTablesOfImport`).
140
+ */
141
+ export async function encodeImportStreams(
142
+ result: ImportResult,
143
+ builtBy?: SealedStoreBuiltBy,
144
+ ): Promise<EncodedImportStream[]> {
145
+ const { encodeSealedUpload } = await import('./sealed-encode.js');
146
+ const names = Object.keys(result.streams).sort((a, b) =>
147
+ a === DEFAULT_IMPORT_STREAM ? -1 : b === DEFAULT_IMPORT_STREAM ? 1 : 0,
148
+ );
149
+ const out: EncodedImportStream[] = [];
150
+ for (const name of names) {
151
+ const content = result.streams[name]!;
152
+ const entries = importContentEntries(content);
153
+ if (entries === 0) continue;
154
+ out.push({ name, bytes: await encodeSealedUpload(content, builtBy), entries });
155
+ }
156
+ return out;
157
+ }
158
+
159
+ // --- The staging bundle -------------------------------------------------
160
+ //
161
+ // A runner outside the platform stages every stream of one import as ONE
162
+ // object, so the seal reads one upload and the streams land together or not
163
+ // at all. `BRSB`, a version byte, the stream count, then per stream a u16
164
+ // name length, the name, a u32 byte length, and the bytes.
165
+
166
+ const BUNDLE_MAGIC = 0x42_52_53_42;
167
+ const BUNDLE_VERSION = 1;
168
+
169
+ export function encodeStreamBundle(
170
+ streams: readonly { name: string; bytes: Uint8Array }[],
171
+ ): Uint8Array {
172
+ const encoder = new TextEncoder();
173
+ const names = streams.map((s) => encoder.encode(s.name));
174
+ let total = 9;
175
+ for (let i = 0; i < streams.length; i += 1) {
176
+ total += 2 + names[i]!.byteLength + 4 + streams[i]!.bytes.byteLength;
177
+ }
178
+ const out = new Uint8Array(total);
179
+ const view = new DataView(out.buffer);
180
+ view.setUint32(0, BUNDLE_MAGIC);
181
+ view.setUint8(4, BUNDLE_VERSION);
182
+ view.setUint32(5, streams.length);
183
+ let at = 9;
184
+ for (let i = 0; i < streams.length; i += 1) {
185
+ const name = names[i]!;
186
+ view.setUint16(at, name.byteLength);
187
+ at += 2;
188
+ out.set(name, at);
189
+ at += name.byteLength;
190
+ const bytes = streams[i]!.bytes;
191
+ view.setUint32(at, bytes.byteLength);
192
+ at += 4;
193
+ out.set(bytes, at);
194
+ at += bytes.byteLength;
195
+ }
196
+ return out;
197
+ }
198
+
199
+ /** The streams a bundle carries, or null for bytes that are not a bundle this
200
+ * reader can read. */
201
+ export function decodeStreamBundle(
202
+ bytes: Uint8Array,
203
+ ): { name: string; bytes: Uint8Array }[] | null {
204
+ if (bytes.byteLength < 9) return null;
205
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
206
+ if (view.getUint32(0) !== BUNDLE_MAGIC || view.getUint8(4) !== BUNDLE_VERSION) {
207
+ return null;
208
+ }
209
+ const count = view.getUint32(5);
210
+ const decoder = new TextDecoder();
211
+ const out: { name: string; bytes: Uint8Array }[] = [];
212
+ let at = 9;
213
+ for (let i = 0; i < count; i += 1) {
214
+ if (at + 2 > bytes.byteLength) return null;
215
+ const nameLength = view.getUint16(at);
216
+ at += 2;
217
+ if (at + nameLength + 4 > bytes.byteLength) return null;
218
+ const name = decoder.decode(bytes.subarray(at, at + nameLength));
219
+ at += nameLength;
220
+ const length = view.getUint32(at);
221
+ at += 4;
222
+ if (at + length > bytes.byteLength) return null;
223
+ out.push({ name, bytes: bytes.subarray(at, at + length) });
224
+ at += length;
225
+ }
226
+ return at === bytes.byteLength ? out : null;
227
+ }