@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,89 @@
1
+ import type { Document } from './documents.js';
2
+ import type { DocumentSourceBlob } from './documents-namespace.js';
3
+ import { BrassError } from './errors.js';
4
+
5
+ // The app's import half for a file Brass does not parse: given the stored
6
+ // source file and the open document, parse the bytes and write the records
7
+ // through the ordinary handles. Register one at boot with
8
+ // `Brass.registerImporter`, beside `Brass.registerSerializer`.
9
+ //
10
+ // The SDK drives the protocol: on each open it asks the platform for the
11
+ // import claim, and only the winning client fetches the source and runs
12
+ // the importer. Every other open client receives the records through its
13
+ // normal subscription, and completing the import stamps the document with
14
+ // the app's declared schema.
15
+ export type DocumentImporter = (
16
+ source: DocumentSourceBlob,
17
+ doc: Document,
18
+ ) => void | Promise<void>;
19
+
20
+ // Wire shape of `POST /documents/:docId/import-claim`'s 200 responses.
21
+ // `claimed` marks the CAS win; `already_imported` carries the stamped
22
+ // provenance of a finished import. A pending claim by another client
23
+ // arrives as a 409 `BrassError` instead.
24
+ export interface ImportClaimResponse {
25
+ claimed?: boolean;
26
+ already_imported?: boolean;
27
+ schema_ref?: { app_id: string; schema_hash: string };
28
+ }
29
+
30
+ export interface ImportRunnerDeps {
31
+ claimImport(docId: string): Promise<ImportClaimResponse>;
32
+ completeImport(docId: string): Promise<unknown>;
33
+ reportError(error: Error): void;
34
+ }
35
+
36
+ export interface ImportRunner {
37
+ setImporter(importer: DocumentImporter): void;
38
+ attempt(doc: Document): Promise<void>;
39
+ }
40
+
41
+ // Runs the import protocol for one opened document: claim, and on the
42
+ // win fetch + parse + complete. Separated from the client so the
43
+ // protocol is testable against mocked HTTP deps.
44
+ export function createImportRunner(deps: ImportRunnerDeps): ImportRunner {
45
+ let importer: DocumentImporter | null = null;
46
+ // One attempt per Document instance; a reopen mints a new instance,
47
+ // whose claim resolves `already_imported` as a no-op.
48
+ const attempted = new WeakSet<Document>();
49
+ return {
50
+ setImporter(fn: DocumentImporter): void {
51
+ importer = fn;
52
+ },
53
+ async attempt(doc: Document): Promise<void> {
54
+ if (importer === null || doc.id === null || attempted.has(doc)) return;
55
+ attempted.add(doc);
56
+ const run = importer;
57
+ const docId = doc.id;
58
+ let claim: ImportClaimResponse;
59
+ try {
60
+ claim = await deps.claimImport(docId);
61
+ } catch (err) {
62
+ // The common no-op outcomes arrive as 4xx: not a stored source
63
+ // file, read-only access, another client's claim in flight. Only
64
+ // a server fault surfaces.
65
+ if (err instanceof BrassError && err.status !== undefined && err.status < 500) {
66
+ return;
67
+ }
68
+ deps.reportError(asError(err));
69
+ return;
70
+ }
71
+ if (claim.claimed !== true) return;
72
+ // This client won the claim: run the importer, then report
73
+ // completion so the platform stamps the document with the app's
74
+ // schema. A throw leaves the claim to expire, so another client
75
+ // can retry rather than the document staying half-claimed.
76
+ try {
77
+ const source = await doc.sourceBlob();
78
+ await run(source, doc);
79
+ await deps.completeImport(docId);
80
+ } catch (err) {
81
+ deps.reportError(asError(err));
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ function asError(err: unknown): Error {
88
+ return err instanceof Error ? err : new Error(String(err));
89
+ }
package/src/index.ts ADDED
@@ -0,0 +1,267 @@
1
+ import { BrassClient } from './client.js';
2
+ import type {
3
+ CreateDocumentOptions,
4
+ OpenDocumentByIdOptions,
5
+ OpenedDocument,
6
+ } from './open-document.js';
7
+ import type { SelectDocumentResult } from './hosted-ui.js';
8
+ import type { DocumentsNamespace } from './documents-namespace.js';
9
+ import type { DocumentImporter } from './importer.js';
10
+ import type { DocumentSerializer } from './serializer.js';
11
+ import type { DocumentSession } from './session.js';
12
+ import type {
13
+ AuthStatus,
14
+ ErrorListener,
15
+ BrassInitOptions,
16
+ BrassUser,
17
+ UserChangeListener,
18
+ } from './types.js';
19
+
20
+ export type {
21
+ AuthStatus,
22
+ ErrorListener,
23
+ BrassInitOptions,
24
+ BrassUser,
25
+ UserChangeListener,
26
+ };
27
+ export { BrassError, type BrassErrorCode } from './errors.js';
28
+ export {
29
+ type AcquireOrganizationResult,
30
+ type HostedUiLauncher,
31
+ type HostedUiLauncherOptions,
32
+ type HostedUiTheme,
33
+ type SelectDocumentResult,
34
+ } from './hosted-ui.js';
35
+ export {
36
+ type DocumentMember,
37
+ type DocumentRecord,
38
+ type DocumentRole,
39
+ type DocumentCheckpoint,
40
+ type DocumentSourceBlob,
41
+ type DocumentSourceStream,
42
+ type DocumentStreamInfo,
43
+ type DocumentSummary,
44
+ type DocumentThumbnail,
45
+ type DocumentsNamespace,
46
+ type GetThumbnailOptions,
47
+ type SetThumbnailInput,
48
+ } from './documents-namespace.js';
49
+ export {
50
+ type CanvasSource,
51
+ type Command,
52
+ type CommandGroup,
53
+ type CommandSurface,
54
+ type DocumentSession,
55
+ type DocumentSessionDeps,
56
+ type SessionOpenOptions,
57
+ type SessionStatus,
58
+ } from './session.js';
59
+ export type {
60
+ SessionToolbarHandle,
61
+ SessionHeaderHandle,
62
+ SessionToolbarOptions,
63
+ MountedPiece,
64
+ } from './session-toolbar.js';
65
+ import type {
66
+ SessionToolbarHandle,
67
+ SessionHeaderHandle,
68
+ SessionToolbarOptions,
69
+ MountedPiece,
70
+ } from './session-toolbar.js';
71
+ export {
72
+ Document,
73
+ type DocumentMutator,
74
+ type DocumentOptions,
75
+ type DocumentSubscriber,
76
+ type SyntheticSubscribeEvent,
77
+ type DocumentUnsubscribe,
78
+ type DocumentGoneListener,
79
+ type DocumentGoneReason,
80
+ type DocumentGoneUnsubscribe,
81
+ type DocumentWorkStatus,
82
+ type DocumentStatusListener,
83
+ type DocumentStatusUnsubscribe,
84
+ type DocumentDriveStatus,
85
+ type DocumentDriveStatusListener,
86
+ type DocumentDriveStatusUnsubscribe,
87
+ type DocumentDriveTrackingState,
88
+ type DriveMirrorDirection,
89
+ type EntityProxy,
90
+ type UndoState,
91
+ type UndoStateSubscriber,
92
+ } from './documents.js';
93
+ export type {
94
+ Change,
95
+ ChangeOrigin,
96
+ ChangeSource,
97
+ HandleUnsubscribe,
98
+ ListHandle,
99
+ ListSubscriber,
100
+ MapHandle,
101
+ MapSubscriber,
102
+ OrderedMapHandle,
103
+ } from './handles.js';
104
+ export type { UndoControllerOptions } from './undo.js';
105
+ export type { DocumentSerializer } from './serializer.js';
106
+ export type { DocumentImporter } from './importer.js';
107
+ export { DocumentStream, type StreamRuntime } from './document-streams.js';
108
+ export {
109
+ type CreateDocumentFn,
110
+ type CreateDocumentOptions,
111
+ type DocumentAtVersionResponse,
112
+ type DocumentDriveSummaryWire,
113
+ type DocumentFactory,
114
+ type DocumentSnapshotResponse,
115
+ type OpenDocumentFactoryOptions,
116
+ type OpenDocumentByIdFn,
117
+ type OpenDocumentByIdOptions,
118
+ type OpenDocumentWsClient,
119
+ type OpenedDocument,
120
+ } from './open-document.js';
121
+ export type {
122
+ PresenceChangeListener,
123
+ PresenceClient,
124
+ PresenceParticipant,
125
+ PresenceSetInput,
126
+ PresenceState,
127
+ } from './presence.js';
128
+
129
+ const client = new BrassClient();
130
+
131
+ // A Brass-hosted bundle (`*.onbrass.app`) resolves its app id and
132
+ // environment from the serving slot, so it uses the zero-argument form;
133
+ // everywhere else the app id is required.
134
+ function init(options?: BrassInitOptions): Promise<void>;
135
+ function init(appId: string, options?: BrassInitOptions): Promise<void>;
136
+ function init(
137
+ appIdOrOptions?: string | BrassInitOptions,
138
+ options?: BrassInitOptions,
139
+ ): Promise<void> {
140
+ if (typeof appIdOrOptions === 'string') return client.init(appIdOrOptions, options);
141
+ return client.init(undefined, appIdOrOptions);
142
+ }
143
+
144
+ export const Brass = {
145
+ init,
146
+ signIn(): void {
147
+ client.signIn();
148
+ },
149
+ signOut(): void {
150
+ client.signOut();
151
+ },
152
+ get user(): BrassUser | null {
153
+ return client.currentUser;
154
+ },
155
+ // Tri-state auth lifecycle: `'unknown'` until `Brass.init` settles,
156
+ // then `'signed-in'` / `'signed-out'`. Gate the sign-in CTA on this,
157
+ // not on `!Brass.user`: a falsy `user` during `'unknown'` is "not
158
+ // checked yet", so gating on `!user` flashes the CTA at signed-in
159
+ // users on reload. Render the CTA only on `'signed-out'`.
160
+ get authStatus(): AuthStatus {
161
+ return client.authStatus;
162
+ },
163
+ onUserChange(listener: UserChangeListener): () => void {
164
+ return client.onUserChange(listener);
165
+ },
166
+ onError(listener: ErrorListener): () => void {
167
+ return client.onError(listener);
168
+ },
169
+ getAccessToken(): Promise<string | null> {
170
+ return client.getAccessToken();
171
+ },
172
+ // Metadata read for a known id (`get`) plus thumbnails / branches /
173
+ // pushToDrive. Create / move / list / grant access are hosted-only.
174
+ get documents(): DocumentsNamespace {
175
+ return client.documents;
176
+ },
177
+ // The document session: owns the active-document lifecycle and exposes
178
+ // a flat command list to render. Accessible before `Brass.init` so a
179
+ // boot flow can subscribe first.
180
+ get session(): DocumentSession {
181
+ return client.session;
182
+ },
183
+ // SDK-rendered document toolbar: file menu, title, location, account
184
+ // menu, presence roster into `container`. The app fills the returned
185
+ // `tools` slot with its own buttons.
186
+ mountToolbar(
187
+ container: HTMLElement,
188
+ options?: SessionToolbarOptions,
189
+ ): SessionToolbarHandle {
190
+ return client.mountToolbar(container, options);
191
+ },
192
+ // The bare header shell, for an app that wants the SDK's responsive
193
+ // layout but fills the clusters itself. Mount pieces into the returned
194
+ // `start` / `tools` / `end` slots; the account avatar in `end` stays
195
+ // pinned top-right on mobile, never wrapping when the tools wrap.
196
+ mountHeader(container: HTMLElement): SessionHeaderHandle {
197
+ return client.mountHeader(container);
198
+ },
199
+ // Composable header pieces, for an app that builds its own shell and
200
+ // wants the standard clusters (account menu / presence / title / file
201
+ // menu / undo-redo) without the whole `mountToolbar` bar. Each renders
202
+ // one cluster and returns `{ destroy(): void }`.
203
+ mountFileMenu(container: HTMLElement): MountedPiece {
204
+ return client.mountFileMenu(container);
205
+ },
206
+ mountDocumentTitle(container: HTMLElement): MountedPiece {
207
+ return client.mountDocumentTitle(container);
208
+ },
209
+ mountUndoRedo(container: HTMLElement): MountedPiece {
210
+ return client.mountUndoRedo(container);
211
+ },
212
+ mountPresenceRoster(container: HTMLElement): MountedPiece {
213
+ return client.mountPresenceRoster(container);
214
+ },
215
+ mountDriveSync(container: HTMLElement): MountedPiece {
216
+ return client.mountDriveSync(container);
217
+ },
218
+ mountAccountMenu(container: HTMLElement): MountedPiece {
219
+ return client.mountAccountMenu(container);
220
+ },
221
+ // The historical-version preview banner (return-to-live during a hosted
222
+ // `history` preview). `mountToolbar` / `mountHeader` already include it, so
223
+ // this is only for an app that builds fully custom chrome.
224
+ mountVersionPreview(container: HTMLElement): MountedPiece {
225
+ return client.mountVersionPreview(container);
226
+ },
227
+ selectDocument(): Promise<SelectDocumentResult | null> {
228
+ return client.selectDocument();
229
+ },
230
+ // Opens the hosted multi-tier share dialog (document / project /
231
+ // organization in one UI) focused on the given document.
232
+ shareDocument(docId: string): Promise<void> {
233
+ return client.shareDocument(docId);
234
+ },
235
+ openDocumentById(
236
+ docId: string,
237
+ options?: OpenDocumentByIdOptions,
238
+ ): Promise<OpenedDocument> {
239
+ return client.openDocumentById(docId, options);
240
+ },
241
+ // `true` when the page was opened as an anonymous link guest
242
+ // (`?brass_link=`): no signed-in user, a read-only document. Apps render
243
+ // their working surface for a guest as they do for a signed-in user (the
244
+ // document is read-only) and show their sign-in CTA only when neither.
245
+ get isGuest(): boolean {
246
+ return client.isGuest;
247
+ },
248
+ createDocument(options?: CreateDocumentOptions): Promise<OpenedDocument> {
249
+ return client.createDocument(options);
250
+ },
251
+ // Register the app's export backend: serialises a blob document back
252
+ // to its source file's bytes. The platform calls it when a user
253
+ // downloads from a hosted surface (the app never initiates export).
254
+ // Call once at boot; an app with no serializer cannot export blobs.
255
+ registerSerializer(serializer: DocumentSerializer): void {
256
+ client.registerSerializer(serializer);
257
+ },
258
+ // Register the app's import backend: parses a stored source file into
259
+ // the document's records. Brass runs it when a document's file needs
260
+ // importing; the records reach every open client live. Call once at
261
+ // boot beside `registerSerializer`.
262
+ registerImporter(importer: DocumentImporter): void {
263
+ client.registerImporter(importer);
264
+ },
265
+ };
266
+
267
+ export default Brass;
@@ -0,0 +1,15 @@
1
+ // Single source of truth for the set of keys an integration needs to
2
+ // plug into `Brass.init`. When you add a new field to `Brass.init`,
3
+ // add the key here first.
4
+
5
+ // Keys used in the prompt block the dashboard renders. The `app_id`
6
+ // line carries the app-specific id; the URL lines carry the env's
7
+ // auth / data / WS origins.
8
+ export const BRASS_INIT_ARG_KEYS = [
9
+ 'app_id',
10
+ 'auth_base_url',
11
+ 'api_base_url',
12
+ 'ws_base_url',
13
+ ] as const;
14
+
15
+ export type BrassInitArgKey = (typeof BRASS_INIT_ARG_KEYS)[number];
@@ -0,0 +1,23 @@
1
+ // The entry point Brass's own surfaces import
2
+ // (`@brass-build/client/internal`).
3
+ //
4
+ // The dashboard, the operator console, and this package's tests consume
5
+ // the SDK as their client library and reach for primitives that sit
6
+ // below the `Brass` namespace: the client class that namespace wraps,
7
+ // the WS transport, the auth-flow helpers, and the init-key vocabulary
8
+ // the dashboard quickstart renders. They live on their own subpath so
9
+ // the default import (`@brass-build/client`) offers apps the `Brass`
10
+ // surface the README documents, and an assistant scaffolding from that
11
+ // README stays on it.
12
+
13
+ export { BrassClient } from './client.js';
14
+ export { BrassWsClient } from './ws-client.js';
15
+ export {
16
+ buildSignInUrl,
17
+ refreshTokens,
18
+ type RefreshCredential,
19
+ type RefreshResult,
20
+ type TokenSet,
21
+ } from './auth-flow.js';
22
+ export { decodeIdToken } from './jwt.js';
23
+ export { BRASS_INIT_ARG_KEYS, type BrassInitArgKey } from './init-keys.js';
package/src/jwt.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { BrassUser } from './types.js';
2
+
3
+ export function decodeIdToken(idToken: string): BrassUser | null {
4
+ const parts = idToken.split('.');
5
+ if (parts.length !== 3) return null;
6
+ const payloadPart = parts[1];
7
+ if (!payloadPart) return null;
8
+ try {
9
+ const json = atob(payloadPart.replace(/-/g, '+').replace(/_/g, '/'));
10
+ const claims = JSON.parse(json) as { sub?: unknown; email?: unknown };
11
+ if (typeof claims.sub !== 'string') return null;
12
+ const user: BrassUser = { sub: claims.sub };
13
+ if (typeof claims.email === 'string') user.email = claims.email;
14
+ return user;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
@@ -0,0 +1,43 @@
1
+ // The organization a launch came from. Brass appends
2
+ // `?brass_org=<organization_id>` to the app URL it opens from an
3
+ // organization's dashboard, so the first document an app creates lands in
4
+ // the organization the person was standing in when they clicked Open. A
5
+ // member of several organizations has already answered "which one" with
6
+ // that click; without the parameter the create reaches the server with no
7
+ // destination and comes back asking again.
8
+ //
9
+ // `brass_` prefixed like the platform's other boot parameters so it cannot
10
+ // collide with an app's own query string, and stripped from the address bar
11
+ // on read so it does not linger in history or a referrer.
12
+ //
13
+ // The dashboard writes this parameter in `src/lib/open-app-url.ts`; the
14
+ // published SDK cannot import the workspace, so the name is spelled in both
15
+ // places.
16
+
17
+ export const LAUNCH_ORGANIZATION_PARAM = 'brass_org';
18
+
19
+ // Pure + search-string-in so it stays testable, mirroring
20
+ // `readGuestLinkToken(search)`.
21
+ export function readLaunchOrganizationId(search: string): string | null {
22
+ const value = new URLSearchParams(search).get(LAUNCH_ORGANIZATION_PARAM);
23
+ const trimmed = value?.trim() ?? '';
24
+ return trimmed === '' ? null : trimmed;
25
+ }
26
+
27
+ // Reads the parameter and strips it from the visible URL in the same tick.
28
+ // Best-effort on the strip: an exotic URL or an unavailable history API
29
+ // leaves the parameter in the address bar, which costs nothing, the value
30
+ // is already in hand.
31
+ export function consumeLaunchOrganizationId(): string | null {
32
+ if (typeof window === 'undefined') return null;
33
+ const organizationId = readLaunchOrganizationId(window.location.search);
34
+ if (!organizationId) return null;
35
+ try {
36
+ const url = new URL(window.location.href);
37
+ url.searchParams.delete(LAUNCH_ORGANIZATION_PARAM);
38
+ window.history.replaceState(window.history.state, '', url.toString());
39
+ } catch {
40
+ /* address-bar tidiness only */
41
+ }
42
+ return organizationId;
43
+ }
@@ -0,0 +1,60 @@
1
+ // Per-app "a Brass session probably exists for this app" hint.
2
+ //
3
+ // The platform session lives in an httpOnly cross-site cookie the app's
4
+ // origin can't read, so on a cold load the SDK would POST `/refresh`
5
+ // blindly; for a signed-out visitor that 401s, which the browser logs to
6
+ // the console even though the SDK handles it cleanly. The hint lets the SDK
7
+ // skip the probe when it has no reason to believe a session exists.
8
+ //
9
+ // Set after any successful refresh, cleared on sign-out or an empty
10
+ // refresh. localStorage (not sessionStorage) so a returning, already-
11
+ // approved user gets a silent restore in a brand-new tab. The value is a
12
+ // non-secret boolean flag (the real session is the server-side row gated by
13
+ // `approved_apps`, useless without the cookie), so readable storage leaks
14
+ // nothing. Keyed per app id.
15
+
16
+ const KEY_PREFIX = '__brass_login_hint_';
17
+
18
+ function getStorage(): Storage | null {
19
+ if (typeof localStorage === 'undefined') return null;
20
+ try {
21
+ return localStorage;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function key(appId: string): string {
28
+ return `${KEY_PREFIX}${appId}`;
29
+ }
30
+
31
+ export function storeLoginHint(appId: string): void {
32
+ const storage = getStorage();
33
+ if (!storage) return;
34
+ try {
35
+ storage.setItem(key(appId), '1');
36
+ } catch {
37
+ // Quota / disabled storage. The hint is a best-effort optimisation;
38
+ // without it the SDK probes /refresh unconditionally.
39
+ }
40
+ }
41
+
42
+ export function readLoginHint(appId: string): boolean {
43
+ const storage = getStorage();
44
+ if (!storage) return false;
45
+ try {
46
+ return storage.getItem(key(appId)) === '1';
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ export function clearLoginHint(appId: string): void {
53
+ const storage = getStorage();
54
+ if (!storage) return;
55
+ try {
56
+ storage.removeItem(key(appId));
57
+ } catch {
58
+ // See storeLoginHint.
59
+ }
60
+ }
@@ -0,0 +1,63 @@
1
+ // Committing a very large mutation as it goes, so an app can write a
2
+ // file's worth of records through the same `mutate` it uses for one.
3
+ //
4
+ // Encoding a commit costs more per record the more containers that commit
5
+ // creates: 20,000 records in one commit take ~60s and produce an op too
6
+ // large to store, where the same records committed every ~500 take under
7
+ // 3s (`scripts/bench/`). Above the budget below, a mutation therefore
8
+ // commits and continues.
9
+ //
10
+ // The consequence a caller can observe is that peers receive such a write
11
+ // as several updates instead of one, so they can see it part-applied. It
12
+ // converges normally and stays one undo step.
13
+
14
+ // A batch closes once the values assigned since the last commit reach
15
+ // roughly this much JSON, which keeps each op in the tens of KB: an
16
+ // encoded op runs close to the JSON length of its values, and base64 adds
17
+ // a third. Budgeting on size rather than on a record count is what keeps
18
+ // that true for narrow and wide records alike, so where it splits depends
19
+ // on width (measured: ~800 spreadsheet cells, ~200 wider rows).
20
+ export const MUTATE_BATCH_JSON_BUDGET_CHARS = 48 * 1024;
21
+
22
+ /** Rough size of one assigned value. Runs on every assignment, so it only
23
+ * has to be proportional; an unserializable value counts as nominal
24
+ * rather than throwing, since the assignment itself is where that error
25
+ * belongs. */
26
+ function approximateChars(value: unknown): number {
27
+ if (value === null || value === undefined) return 8;
28
+ const t = typeof value;
29
+ if (t === 'number' || t === 'boolean') return 8;
30
+ if (t === 'string') return (value as string).length + 2;
31
+ try {
32
+ return JSON.stringify(value)?.length ?? 64;
33
+ } catch {
34
+ return 64;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Decides when a mutation should commit. The caller supplies what a commit
40
+ * means, since an ordered map reconciles its order list first.
41
+ */
42
+ export class MutateBatcher {
43
+ private _chars = 0;
44
+
45
+ constructor(private readonly _flush: () => void) {}
46
+
47
+ /** Account for one assignment, from the proxy's set trap. */
48
+ record(value: unknown): void {
49
+ const chars = approximateChars(value);
50
+ // Commit BEFORE adding, so one assignment larger than the whole budget
51
+ // lands in a batch of its own instead of dragging its neighbours over
52
+ // the line with it.
53
+ if (this._chars > 0 && this._chars + chars > MUTATE_BATCH_JSON_BUDGET_CHARS) {
54
+ this._flush();
55
+ this._chars = 0;
56
+ }
57
+ this._chars += chars;
58
+ }
59
+
60
+ recordDelete(): void {
61
+ this.record(null);
62
+ }
63
+ }