@brass-build/client 0.2.0 → 0.4.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 (168) hide show
  1. package/AGENTS.md +26 -5
  2. package/CHANGELOG.md +61 -2
  3. package/README.md +247 -87
  4. package/dist/acting-organization.d.ts +131 -0
  5. package/dist/acting-organization.d.ts.map +1 -0
  6. package/dist/acting-organization.js +278 -0
  7. package/dist/acting-organization.js.map +1 -0
  8. package/dist/api-headers.js +7 -7
  9. package/dist/api-headers.js.map +1 -1
  10. package/dist/call-service.d.ts +74 -0
  11. package/dist/call-service.d.ts.map +1 -0
  12. package/dist/call-service.js +253 -0
  13. package/dist/call-service.js.map +1 -0
  14. package/dist/client.d.ts +29 -0
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +583 -12
  17. package/dist/client.js.map +1 -1
  18. package/dist/document-checkpoints.d.ts +1 -0
  19. package/dist/document-checkpoints.d.ts.map +1 -1
  20. package/dist/document-checkpoints.js +27 -0
  21. package/dist/document-checkpoints.js.map +1 -1
  22. package/dist/document-contents.d.ts +15 -0
  23. package/dist/document-contents.d.ts.map +1 -0
  24. package/dist/document-contents.js +88 -0
  25. package/dist/document-contents.js.map +1 -0
  26. package/dist/document-signals.d.ts +31 -1
  27. package/dist/document-signals.d.ts.map +1 -1
  28. package/dist/document-signals.js +59 -0
  29. package/dist/document-signals.js.map +1 -1
  30. package/dist/document-streams.d.ts +4 -2
  31. package/dist/document-streams.d.ts.map +1 -1
  32. package/dist/document-streams.js +6 -0
  33. package/dist/document-streams.js.map +1 -1
  34. package/dist/document-thumbnail-capture.d.ts +20 -0
  35. package/dist/document-thumbnail-capture.d.ts.map +1 -1
  36. package/dist/document-thumbnail-capture.js +247 -9
  37. package/dist/document-thumbnail-capture.js.map +1 -1
  38. package/dist/documents-namespace.d.ts.map +1 -1
  39. package/dist/documents-namespace.js +3 -0
  40. package/dist/documents-namespace.js.map +1 -1
  41. package/dist/documents.d.ts +51 -3
  42. package/dist/documents.d.ts.map +1 -1
  43. package/dist/documents.js +415 -36
  44. package/dist/documents.js.map +1 -1
  45. package/dist/export-guest.d.ts.map +1 -1
  46. package/dist/export-guest.js +5 -0
  47. package/dist/export-guest.js.map +1 -1
  48. package/dist/handle-types.d.ts +23 -0
  49. package/dist/handle-types.d.ts.map +1 -1
  50. package/dist/handles.d.ts +1 -1
  51. package/dist/handles.d.ts.map +1 -1
  52. package/dist/handles.js +3 -3
  53. package/dist/handles.js.map +1 -1
  54. package/dist/hosted-ui.d.ts +10 -0
  55. package/dist/hosted-ui.d.ts.map +1 -1
  56. package/dist/hosted-ui.js +115 -30
  57. package/dist/hosted-ui.js.map +1 -1
  58. package/dist/importer.d.ts.map +1 -1
  59. package/dist/importer.js +28 -66
  60. package/dist/importer.js.map +1 -1
  61. package/dist/index.d.ts +8 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +25 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/internal.d.ts +2 -0
  66. package/dist/internal.d.ts.map +1 -1
  67. package/dist/internal.js +2 -0
  68. package/dist/internal.js.map +1 -1
  69. package/dist/mutate-batching.js +2 -2
  70. package/dist/mutate-batching.js.map +1 -1
  71. package/dist/open-document.d.ts +16 -1
  72. package/dist/open-document.d.ts.map +1 -1
  73. package/dist/open-document.js +123 -35
  74. package/dist/open-document.js.map +1 -1
  75. package/dist/presence.d.ts +10 -0
  76. package/dist/presence.d.ts.map +1 -1
  77. package/dist/presence.js.map +1 -1
  78. package/dist/record-paging.d.ts +13 -14
  79. package/dist/record-paging.d.ts.map +1 -1
  80. package/dist/record-paging.js +13 -14
  81. package/dist/record-paging.js.map +1 -1
  82. package/dist/serializer.d.ts +3 -3
  83. package/dist/session-toolbar.d.ts +9 -0
  84. package/dist/session-toolbar.d.ts.map +1 -1
  85. package/dist/session-toolbar.js +191 -21
  86. package/dist/session-toolbar.js.map +1 -1
  87. package/dist/tail-ops.js +2 -2
  88. package/dist/tail-ops.js.map +1 -1
  89. package/dist/types.d.ts +0 -42
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/undo.d.ts +1 -1
  92. package/dist/undo.d.ts.map +1 -1
  93. package/dist/undo.js +1 -1
  94. package/dist/undo.js.map +1 -1
  95. package/dist/version-bar.d.ts.map +1 -1
  96. package/dist/version-bar.js +75 -4
  97. package/dist/version-bar.js.map +1 -1
  98. package/dist/version.d.ts +2 -2
  99. package/dist/version.js +1 -1
  100. package/dist/wasm-ready-gate.d.ts +13 -0
  101. package/dist/wasm-ready-gate.d.ts.map +1 -0
  102. package/dist/wasm-ready-gate.js +28 -0
  103. package/dist/wasm-ready-gate.js.map +1 -0
  104. package/dist/wasm-runtime.browser.d.ts +11 -0
  105. package/dist/wasm-runtime.browser.d.ts.map +1 -0
  106. package/dist/wasm-runtime.browser.js +44 -0
  107. package/dist/wasm-runtime.browser.js.map +1 -0
  108. package/dist/wasm-runtime.d.ts +6 -0
  109. package/dist/wasm-runtime.d.ts.map +1 -0
  110. package/dist/wasm-runtime.js +20 -0
  111. package/dist/wasm-runtime.js.map +1 -0
  112. package/dist/when-saved.d.ts +2 -0
  113. package/dist/when-saved.d.ts.map +1 -0
  114. package/dist/when-saved.js +70 -0
  115. package/dist/when-saved.js.map +1 -0
  116. package/dist/ws-client.d.ts +4 -0
  117. package/dist/ws-client.d.ts.map +1 -1
  118. package/dist/ws-client.js +70 -1
  119. package/dist/ws-client.js.map +1 -1
  120. package/dist/ws-frame-limit.d.ts.map +1 -1
  121. package/dist/ws-frame-limit.js +9 -10
  122. package/dist/ws-frame-limit.js.map +1 -1
  123. package/docs/app-manifest.md +335 -5
  124. package/docs/custom-chrome.md +7 -2
  125. package/docs/presence.md +12 -0
  126. package/docs/services.md +265 -0
  127. package/docs/text-fields.md +191 -0
  128. package/package.json +4 -5
  129. package/src/acting-organization.ts +396 -0
  130. package/src/api-headers.ts +7 -7
  131. package/src/call-service.ts +354 -0
  132. package/src/client.ts +655 -15
  133. package/src/document-checkpoints.ts +30 -0
  134. package/src/document-contents.ts +104 -0
  135. package/src/document-signals.ts +113 -1
  136. package/src/document-streams.ts +15 -1
  137. package/src/document-thumbnail-capture.ts +253 -9
  138. package/src/documents-namespace.ts +20 -10
  139. package/src/documents.ts +460 -33
  140. package/src/export-guest.ts +5 -0
  141. package/src/handle-types.ts +35 -0
  142. package/src/handles.ts +7 -6
  143. package/src/hosted-ui.ts +155 -29
  144. package/src/importer.ts +28 -62
  145. package/src/index.ts +43 -4
  146. package/src/internal.ts +10 -0
  147. package/src/mutate-batching.ts +2 -2
  148. package/src/open-document.ts +179 -39
  149. package/src/presence.ts +6 -0
  150. package/src/record-paging.ts +13 -14
  151. package/src/serializer.ts +3 -3
  152. package/src/session-toolbar.ts +219 -22
  153. package/src/tail-ops.ts +2 -2
  154. package/src/types.ts +6 -4
  155. package/src/undo.ts +1 -1
  156. package/src/version-bar.ts +77 -7
  157. package/src/version.ts +1 -1
  158. package/src/wasm-ready-gate.ts +30 -0
  159. package/src/wasm-runtime.browser.ts +48 -0
  160. package/src/wasm-runtime.ts +21 -0
  161. package/src/when-saved.ts +66 -0
  162. package/src/ws-client.ts +70 -1
  163. package/src/ws-frame-limit.ts +9 -10
  164. package/dist/vite.d.ts +0 -31
  165. package/dist/vite.d.ts.map +0 -1
  166. package/dist/vite.js +0 -161
  167. package/dist/vite.js.map +0 -1
  168. package/src/vite.ts +0 -214
@@ -8,9 +8,14 @@ a download button on the documents your app writes out.
8
8
  ## Where it goes
9
9
 
10
10
  Put `brass-app.json` in a `/.well-known/` directory at your bundle
11
- root, next to `index.html`. Some bundlers skip dot-directories when
12
- they copy static assets, so confirm the manifest is in your `dist/`
13
- directory after you build.
11
+ root, next to `index.html`. In Vite, `public/.well-known/brass-app.json`
12
+ builds to `dist/.well-known/brass-app.json`. Check it is there after you
13
+ build: a bundler that skips dot-directories drops it silently.
14
+
15
+ `brass publish` reads that same path out of the directory it publishes
16
+ (`<dir>/.well-known/brass-app.json`), falling back to `brass-app.json` in
17
+ the working directory, so it takes your `name` and `client_token` from the
18
+ manifest you already ship.
14
19
 
15
20
  The platform re-reads the manifest after each deploy, so publishing
16
21
  your bundle is what registers it. If Brass hosts your bundle
@@ -32,7 +37,11 @@ no access, so you declare them yourself.
32
37
  "creates": "tracker",
33
38
  "exports": { "content_type": "text/csv", "file_extension": ".csv" },
34
39
  "icon": "/icon.svg",
35
- "schema": { "...": "see schemas.md" }
40
+ "services": [{ "host": "api.anthropic.com", "headers": { "x-api-key": "{secret}" } }],
41
+ "schema": {
42
+ "family": "tracker",
43
+ "streams": { "default": { "type": "object", "required": ["rows"] } }
44
+ }
36
45
  }
37
46
  ```
38
47
 
@@ -59,6 +68,327 @@ Every field is optional, so ship only the ones your app uses.
59
68
  (`"icon": "/icon.svg"`, resolved against your origin), or give an
60
69
  absolute `https://` URL. An emoji (`"icon": "🏗️"`) also renders,
61
70
  though apps in one domain tend to land on the same few.
71
+ - **`services`** names the third-party APIs your app calls. Brass proxies
72
+ each call, using either your credentials or the user's. See
73
+ [Services](#services-for-calling-third-party-apis) below.
62
74
  - **`schema`** declares the record shape your app works with, whether
63
- you invented it or adopted an existing one. See
75
+ you designed it or adopted an existing one. See
64
76
  [`schemas.md`](./schemas.md).
77
+
78
+ ## Services, for calling third-party APIs
79
+
80
+ `services` names the third-party APIs that your app calls. Each service
81
+ declaration includes a host name and an authentication method. Brass then
82
+ proxies each call, using either your credentials or the user's, so no secret
83
+ is exposed in the client.
84
+
85
+ ```json
86
+ {
87
+ "services": [
88
+ { "host": "api.anthropic.com", "headers": { "x-api-key": "{secret}" } },
89
+ { "host": "api.example.com", "headers": { "authorization": "Bearer {secret}" } }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ [`services.md`](./services.md) covers the calling side: what each refusal code
95
+ means, how to prompt for a missing credential, which host to pass for a
96
+ service split across addresses, and how to provide setup instructions for
97
+ the people who supply your credentials.
98
+
99
+ ### Specifying a host
100
+
101
+ `host` is the host name your app passes to `Brass.callService`. It must be a
102
+ public https host: no scheme, no port, no path.
103
+
104
+ Use a wildcard where a service is split across subdomains (`*.example.com`).
105
+ Your app can then call any address under that suffix.
106
+
107
+ Set `credentialPerHost` to give each address its own credential. Without it,
108
+ one credential covers every address under the wildcard. With it, the user
109
+ enters a separate credential for each address, which is what a vendor that
110
+ hosts a separate address per account requires.
111
+
112
+ ```json
113
+ {
114
+ "services": [
115
+ { "host": "*.example.com", "headers": { "authorization": "Bearer {secret}" } },
116
+ {
117
+ "host": "*.myshopify.com",
118
+ "credentialPerHost": true,
119
+ "headers": { "x-shopify-access-token": "{secret}" }
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ `credentialPerHost` is required for a suffix that anyone can register a name
126
+ under. One credential covering `*.herokuapp.com` or `*.pages.dev` could be
127
+ used at an address a stranger created, so Brass rejects that declaration and
128
+ reports it on your next publish. The same rule limits a shared wildcard:
129
+ `*.amazonaws.com` can call `sts.amazonaws.com` and
130
+ `lambda.us-west-2.amazonaws.com`, but not `any-bucket.s3.amazonaws.com`,
131
+ because that name belongs to whoever created the bucket.
132
+
133
+ ### Credential fields
134
+
135
+ A field is one value the person connecting a credential is asked for. Each one
136
+ is a box on the form they fill in.
137
+
138
+ Some authentication methods declare their own fields, and each method below
139
+ lists the ones it declares. Declare any others in `fields`:
140
+
141
+ | Property | |
142
+ | --- | --- |
143
+ | `name` | Required. Identifies the field. |
144
+ | `secret` | `true` by default. Brass encrypts the value and shows only its last few characters afterwards. `false` stores it in the clear and shows it in full. |
145
+ | `label` | What the form calls the field. Use the vendor's own word for it, since that is what the person is reading from. Defaults to the name. |
146
+ | `readable` | `false` by default. `true` returns the value to your app with the service, for a value your app has to include in the request. Such a field is not secret, so `secret` defaults to `false` for it and declaring both is refused. See [Values the app reads](./services.md#values-the-app-reads). |
147
+
148
+ ```json
149
+ {
150
+ "services": [
151
+ {
152
+ "host": "api.example.com",
153
+ "headers": { "x-app": "{app_id}", "x-region": "{region}", "x-key": "{key}" },
154
+ "fields": [
155
+ { "name": "app_id", "secret": false, "label": "Application ID" },
156
+ { "name": "region", "secret": false, "label": "Region" }
157
+ ]
158
+ }
159
+ ]
160
+ }
161
+ ```
162
+
163
+ An entry naming a field the method already declared changes that field's
164
+ properties instead of adding a new one, which is how you relabel one. A field
165
+ the method declares secret stays secret.
166
+
167
+ ### Choosing an authentication method
168
+
169
+ A credential sent in a plain header needs no `auth` block. Every other method
170
+ is an `auth` block naming its `type`.
171
+
172
+ #### `headers`, for a credential sent in a header
173
+
174
+ Each header value is a template, and Brass substitutes a field into each
175
+ placeholder you write. The fields are the names in those placeholders; this
176
+ method declares none of its own.
177
+
178
+ ```json
179
+ {
180
+ "services": [
181
+ {
182
+ "host": "production.plaid.com",
183
+ "headers": {
184
+ "plaid-client-id": "{client_id}",
185
+ "plaid-secret": "{secret}"
186
+ },
187
+ "fields": [{ "name": "client_id", "secret": false, "label": "Client ID" }]
188
+ }
189
+ ]
190
+ }
191
+ ```
192
+
193
+ #### `basic`, for HTTP basic auth
194
+
195
+ Brass sends `authorization: Basic base64(username:password)`.
196
+
197
+ The fields are `username` and `password`.
198
+
199
+ ```json
200
+ { "host": "api.twilio.com", "auth": { "type": "basic" } }
201
+ ```
202
+
203
+ Some services take a fixed word as the user half rather than an account name,
204
+ such as `api` for Mailgun. Set it as `username` on the `auth` block and the
205
+ person is asked for the password alone.
206
+
207
+ ```json
208
+ { "host": "api.mailgun.net", "auth": { "type": "basic", "username": "api" } }
209
+ ```
210
+
211
+ #### `query`, for a credential sent as a query parameter
212
+
213
+ Brass appends these parameters to the query your app wrote, substituting a
214
+ field into each placeholder you write. The fields are the names in those
215
+ placeholders; this method declares none of its own.
216
+
217
+ ```json
218
+ {
219
+ "host": "maps.example.com",
220
+ "auth": { "type": "query", "params": { "key": "{secret}" } }
221
+ }
222
+ ```
223
+
224
+ #### `aws_sigv4`, for a signed AWS request
225
+
226
+ Brass signs the request, so the secret never leaves Brass.
227
+
228
+ The fields are `access_key_id` and `secret_access_key`.
229
+
230
+ ```json
231
+ { "host": "*.amazonaws.com", "auth": { "type": "aws_sigv4" } }
232
+ ```
233
+
234
+ AWS puts the region and the service in most of its host names, and Brass signs
235
+ for whichever the address names. Set `region` and `service` for addresses that
236
+ name neither, such as a bucket or an account id before the service name. Brass
237
+ uses them only where the host name does not say, so one credential still
238
+ covers a whole suffix.
239
+
240
+ #### `oauth2_client_credentials`, for a token from your client credentials
241
+
242
+ You register a client with the vendor. Brass posts the client id and secret to
243
+ the token endpoint, receives a short-lived token, and sends that token with
244
+ each call. Set `clientAuth` to `body` or `basic` to say where the id and
245
+ secret go; vendors differ, and many accept only one.
246
+
247
+ The fields are `client_id` and `client_secret`.
248
+
249
+ ```json
250
+ {
251
+ "host": "api.xero.com",
252
+ "auth": {
253
+ "type": "oauth2_client_credentials",
254
+ "tokenHost": "identity.xero.com",
255
+ "tokenPath": "/connect/token",
256
+ "clientAuth": "basic",
257
+ "scope": "accounting.transactions"
258
+ }
259
+ }
260
+ ```
261
+
262
+ #### `jwt_bearer`, for a token from a signed assertion
263
+
264
+ Brass signs a short-lived assertion with a private key, so the key never
265
+ leaves Brass. Set `audience` to the string the vendor requires in the
266
+ assertion. Vendors are strict about this value, so declare it rather than
267
+ letting Brass build one.
268
+
269
+ The fields are `issuer` and `private_key`.
270
+
271
+ ```json
272
+ {
273
+ "host": "oauth2.googleapis.com",
274
+ "auth": {
275
+ "type": "jwt_bearer",
276
+ "tokenPath": "/token",
277
+ "audience": "https://oauth2.googleapis.com/token",
278
+ "algorithm": "RS256"
279
+ }
280
+ }
281
+ ```
282
+
283
+ Both token methods take two more fields:
284
+
285
+ - `tokenPath` names the path on the credential's own host. Set `tokenUrl`
286
+ instead to name the whole address. `tokenHost` defaults to the credential's
287
+ own host.
288
+ Set it to post the exchange to a different host. Brass allows this for a
289
+ credential you supply, and rejects it for one the user supplies, unless the
290
+ user chose the method. Otherwise your manifest would decide where their
291
+ secret is sent.
292
+ - `tokenBody` sets how the exchange is posted: `form` (the default) or `json`.
293
+ Set `json` when your vendor's token endpoint takes a JSON body.
294
+
295
+ #### `oauth2_authorization_code`, for a per-user approval at the vendor
296
+
297
+ Each user approves at the vendor, and Brass stores the result for that user
298
+ alone. Your app then reaches only what that user can reach. Use this at a host
299
+ whose permissions differ per user, where one shared account would reach
300
+ everything.
301
+
302
+ The fields are `client_id` and `client_secret`, which are your registered
303
+ OAuth client rather than anything the approving user enters.
304
+
305
+ ```json
306
+ {
307
+ "host": "api.example.com",
308
+ "auth": {
309
+ "type": "oauth2_authorization_code",
310
+ "authorizationUrl": "https://app.example.com/oauth/authorize",
311
+ "tokenUrl": "https://api.example.com/oauth/token",
312
+ "clientAuth": "basic"
313
+ }
314
+ }
315
+ ```
316
+
317
+ - `clientAuth` sets where your client id and secret go on the exchange: `body`
318
+ or `basic`.
319
+ - `authorizeParams` adds parameters the vendor requires on the approval
320
+ request. Many vendors issue no refresh token without one, and the connection
321
+ then stops working when the first token expires. Google requires
322
+ `{"access_type": "offline", "prompt": "consent"}`. Read your vendor's
323
+ documentation on refresh tokens before you ship this.
324
+ - `pkce` defaults to `true`. Set `false` for a vendor that rejects the
325
+ challenge.
326
+
327
+ You supply an OAuth client registered with the vendor, entering its id and
328
+ secret like any other credential. Register
329
+ `https://brass.build/services/oauth/callback` as the redirect URL. That one
330
+ address serves every service, and there is nothing to configure in Brass.
331
+
332
+ Calls return `409` with `credential_required` until the user approves.
333
+ [`services.md`](./services.md) covers prompting them.
334
+
335
+ #### An OpenAPI security scheme
336
+
337
+ `auth` also accepts an OpenAPI `securityScheme`, so you can paste what your
338
+ vendor's spec already says. Brass reads it as the equivalent method above:
339
+ `apiKey` and `http` `bearer` take a single `secret` field, sent in the place
340
+ the spec names; `http` `basic` reads as `basic`; and the `clientCredentials`
341
+ and `authorizationCode` flows read as their OAuth methods, with those methods'
342
+ fields.
343
+
344
+ ```json
345
+ { "host": "api.example.com", "auth": { "type": "apiKey", "in": "header", "name": "X-API-Key" } }
346
+ ```
347
+
348
+ The `authorizationCode` flow is the same grant as `oauth2_authorization_code`
349
+ above. A spec carries no `clientAuth`, `authorizeParams` or `pkce`, so set
350
+ those alongside `flows`:
351
+
352
+ ```json
353
+ {
354
+ "host": "api.example.com",
355
+ "auth": {
356
+ "type": "oauth2",
357
+ "flows": {
358
+ "authorizationCode": {
359
+ "authorizationUrl": "https://app.example.com/oauth/authorize",
360
+ "tokenUrl": "https://api.example.com/oauth/token",
361
+ "scopes": { "contacts.read": "Read contacts" }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ ```
367
+
368
+ #### More than one method at one host
369
+
370
+ Set `auth` to an array where a host accepts more than one kind of credential.
371
+ A user with an API key can then use it at a host you declared for approval:
372
+
373
+ ```json
374
+ {
375
+ "host": "api.example.com",
376
+ "auth": [
377
+ { "type": "oauth2", "flows": { "authorizationCode": { "...": "..." } } },
378
+ { "type": "apiKey", "in": "header", "name": "X-API-Key" }
379
+ ]
380
+ }
381
+ ```
382
+
383
+ Brass uses your first entry by default. The user can choose another when they
384
+ enter their credential, and Brass then asks for whatever that method needs.
385
+
386
+ Set `authorOnly` on an entry to keep it out of the user's choices. Use it for
387
+ a credential only you can obtain, such as a vendor's internal-integration
388
+ secret, which authenticates as your account rather than theirs.
389
+
390
+ A host often accepts several kinds of credential. A vendor that issues both
391
+ personal tokens and OAuth clients takes either, and they are different
392
+ credentials at one address. Declare the method your app is built around. A
393
+ user who pastes an OAuth client pair where you declared a bearer token is
394
+ asked for the client id as well, and your app keeps calling one host.
@@ -48,8 +48,10 @@ Each piece renders one cluster wired to `Brass.session` and returns
48
48
  - `Brass.mountPresenceRoster` renders the who's-here roster of
49
49
  coloured initials. It is the receiver half only: still publish your
50
50
  own presence state (see the README's "Presence").
51
- - `Brass.mountDriveSync` renders the drive-sync badge. It renders
52
- nothing on a non-drive doc, so it is safe to mount in any header.
51
+ - `Brass.mountDriveSync` renders the drive-sync badge: the sync state
52
+ and publish of a document linked to a drive, and the offer to publish
53
+ one its project's drive has not received yet. It carries the actions
54
+ the open document has, so it is safe in any header.
53
55
  - `Brass.mountAccountMenu` renders the account avatar menu (email, an
54
56
  Open Brass dashboard link, and sign out).
55
57
 
@@ -128,6 +130,9 @@ hook for keyboard shortcuts).
128
130
  Reading the list and calling `run()` wires all of it. You render and
129
131
  invoke, and the SDK owns the behavior:
130
132
 
133
+ - **`new`** creates a document and binds it, asking the platform where
134
+ to put it (`Brass.session.create()` under the hood).
135
+ - **`open`** opens the hosted picker and binds what the user chooses.
131
136
  - **`history`** drives version preview end to end. During a preview
132
137
  `doc.readOnly` is `true`, so gate your own edit controls on it;
133
138
  returning to live is the chrome's job (the `mountVersionPreview`
package/docs/presence.md CHANGED
@@ -67,9 +67,21 @@ interface PresenceState {
67
67
  // The entity ids the user has selected. Use to highlight remote
68
68
  // selections so collaborators see what each other is working on.
69
69
  selection?: string[];
70
+ // Where the user's caret sits in a text container. `key` names the
71
+ // container (`doc.text(key)`), and both offsets count UTF-16 code
72
+ // units into its value: `anchor` where the selection began, `head`
73
+ // where it is now, equal for a plain caret. Use it to draw remote
74
+ // carets in prose, the way `selection` highlights picked entities.
75
+ textCaret?: { key: string; anchor: number; head: number };
70
76
  }
71
77
  ```
72
78
 
79
+ A caret belongs in `state` rather than in `ext` for the same reason a
80
+ selection does: a document of prose is often open in more than one app,
81
+ and a reader wants to see where the other person is working whichever
82
+ app they are each in. Publish it on every selection change, which the
83
+ 10 Hz debounce already paces.
84
+
73
85
  App-specific state (the current tool, a chat bubble, a typing
74
86
  indicator) goes under `ext` instead so it stays namespaced and out of
75
87
  the cross-app conventions. The `ext` block is otherwise free-form:
@@ -0,0 +1,265 @@
1
+ # Calling an outside service
2
+
3
+ The README's [Calling an outside service](../README.md#calling-an-outside-service)
4
+ covers how to make a call. This page covers what each refusal code means,
5
+ how to read values only the user knows, and which host to pass for a service
6
+ split across several addresses. [`app-manifest.md`](./app-manifest.md) covers
7
+ declaring a host and its authentication method.
8
+
9
+ ## What comes back
10
+
11
+ `callService` returns the service's response unchanged, within these limits:
12
+
13
+ - Text only, up to 5 MB, within 25 seconds.
14
+ - No streaming. A streaming endpoint's events all arrive together at the end.
15
+ - For binary data, call an endpoint that returns a URL or base64.
16
+ - Brass does not follow redirects. A `3xx` is returned as
17
+ `response_redirected`, with the service's `location` in the body.
18
+
19
+ `options.method` defaults to `'POST'`.
20
+
21
+ ## Refusal codes
22
+
23
+ When Brass refuses a call rather than sending it, the response body has a
24
+ `code`:
25
+
26
+ ```js
27
+ const res = await Brass.callService(host, path);
28
+ if (!res.ok) {
29
+ const { code } = await res.json();
30
+ if (code === 'credential_required') await Brass.connectService(host);
31
+ }
32
+ ```
33
+
34
+ Each code always comes with the same HTTP status, so branching on the code
35
+ is enough. Two answers carry no code, and both mean the hop to the service
36
+ itself did not complete: a `502` where Brass could not reach the service, and
37
+ a `504` where the service did not answer in time. Other `502`s do carry a
38
+ code, so read the body before treating one as unreachable.
39
+
40
+ **Somebody using your app can supply what is missing.** Call
41
+ `Brass.connectService(host)`. The dialog asks whoever opened it for whatever
42
+ the host needs, and tells anyone who cannot answer who to ask.
43
+
44
+ | `code` | What it means |
45
+ | --------------------- | --------------------------------------------------------------------------------------------------------- |
46
+ | `credential_required` | No credential is connected for this host, or the one connected is not approved for your app, no longer covers what your app sends, or carries no value under a name your app reads back. One code, because the dialog is the answer to all of them. The body's sentence says which, for a person to read. |
47
+
48
+ **Your own key is what is missing.** These are yours to settle on your app's
49
+ settings page, so there is nothing to raise on the page: report the feature as
50
+ unavailable.
51
+
52
+ | `code` | What it means |
53
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
54
+ | `app_credential_not_set` | You have not entered a key for this service yet. This is the refusal you get by default, until you enter one or hand the service over for whoever runs your app to connect. |
55
+ | `app_credential_incomplete` | Your own key carries no value under a name your manifest asks for. |
56
+
57
+ **A stored credential is there and does not work.**
58
+
59
+ | `code` | What it means |
60
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------- |
61
+ | `credential_rejected` | The service rejected the stored credential. |
62
+ | `credential_scheme_unusable` | The host's credential is not usable as configured. |
63
+ | `credential_exchange_failed` | Brass could not reach the service for a token, or got back something that was not one. |
64
+ | `credential_unreadable` | Brass cannot read the credential stored here. Enter it again. |
65
+
66
+ **The call itself is not allowed.** Fix these in your app.
67
+
68
+ | `code` | What it means |
69
+ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
70
+ | `service_not_declared` | Your manifest does not declare that host, or your latest publish has not registered it. |
71
+ | `path_invalid` | The path names its own host, escapes the service with `..`, or cannot be decoded. |
72
+ | `service_headers_invalid` | A header in `options.headers` is one HTTP itself controls (`content-length`, `host`, `connection` and the like), or its name or value cannot go on a header line. |
73
+ | `host_not_public` | The host does not resolve to a public IP address. |
74
+
75
+ **The call is allowed but cannot go through right now.** Some of these clear
76
+ on their own.
77
+
78
+ | `code` | What it means |
79
+ | -------------------- | -------------------------------------------------------------------------------------- |
80
+ | `cap_exceeded` | A cap has been reached. Try again later. |
81
+ | `rate_limited` | Over Brass's own rate limit. Slow down. |
82
+ | `calls_paused` | Whoever owns the account being spent set its cap to zero. Waiting will not clear it. |
83
+ | `vault_unavailable` | Brass could not read the stored key just now. Nothing is wrong with it; make the same call again. |
84
+
85
+ **Brass could not return the response.**
86
+
87
+ | `code` | What it means |
88
+ | -------------------- | --------------------------------------------------------------------- |
89
+ | `response_too_large` | The service returned more than 5 MB. |
90
+ | `response_not_text` | The service returned bytes rather than text. Call an endpoint that returns a URL or base64. |
91
+ | `response_unreadable`| Brass could not read the service's response. |
92
+ | `response_redirected`| The service answered with a redirect. The body carries its `location`. |
93
+ | `service_timeout` | The service did not answer in time. |
94
+ | `service_unreachable`| Brass could not reach the service. |
95
+
96
+ **The app is not the one that may call.** These are about your app's standing
97
+ rather than the credential.
98
+
99
+ | `code` | What it means |
100
+ | -------------------- | --------------------------------------------------------------------- |
101
+ | `app_not_installed` | The organization has not installed your app. `requestMissingCredentials: true` raises the install dialog; otherwise the call returns `403`. |
102
+ | `no_acting_app` | The caller is not acting as an app. |
103
+
104
+ A call made before the app has an organization returns `409 destination_required`.
105
+
106
+ ## Asking for a missing credential
107
+
108
+ `Brass.listServices()` returns each declared host, with `ready: true` where
109
+ Brass has a credential for it. It shows no dialog, so you can call it while
110
+ the page loads. It returns `null` while the app is acting nowhere: nobody is
111
+ signed in, the app has no organization, or it is running in one that has not
112
+ installed it.
113
+
114
+ Each entry also carries `connectable`, which says whether the dialog has
115
+ anything for the person on the page to do at that host. Together the two are
116
+ the whole branch:
117
+
118
+ ```js
119
+ const services = await Brass.listServices();
120
+ const ai = services?.find((s) => s.host === 'api.anthropic.com');
121
+ if (ai?.ready) showSummariseButton();
122
+ else if (ai?.connectable) showConnectCard();
123
+ else showUnavailableCard();
124
+ ```
125
+
126
+ `Brass.connectService(host)` opens the dialog and resolves true once that
127
+ host works. Whoever can supply the credential does it there; anyone else is
128
+ told who to ask. Put it on a button, so the user is asked when they reach for
129
+ the feature:
130
+
131
+ ```js
132
+ if (await Brass.connectService('api.anthropic.com')) showSummariseButton();
133
+ ```
134
+
135
+ Passing `requestMissingCredentials: true` to `callService` opens the same
136
+ dialog from the call, which then goes through once the user answers. It covers
137
+ every refusal the person using your app can answer: `credential_required`, plus
138
+ `credential_rejected` and `credential_scheme_unusable` when the account that
139
+ failed is one they own.
140
+
141
+ A host still waiting for the key you enter as its author
142
+ (`app_credential_not_set`, `app_credential_incomplete`) is yours to settle on
143
+ the app's own settings page, so the call returns that refusal for you to
144
+ handle:
145
+
146
+ ```js
147
+ const res = await Brass.callService(host, path, { requestMissingCredentials: true });
148
+ ```
149
+
150
+ ## Who supplies the credential
151
+
152
+ You choose on your app's settings page whether a host runs on a key of yours
153
+ or on an account whoever runs your app connects, and they can change that
154
+ under you. A host you have just declared starts on a key of yours, so
155
+ `connectable` is false there until you change it. Either way your call is the
156
+ same one: you name a host, and Brass attaches whichever credential is in
157
+ force.
158
+
159
+ Your app reads `connectable` rather than the supplier, because that is the
160
+ split it can act on. Where it is true, the person using your app can
161
+ settle it, so a Connect button leads somewhere; where it is false, the host is
162
+ waiting on you, so there is nothing to raise. Leave the wording about who to
163
+ ask to the dialog, which knows both the supplier and who is reading, where
164
+ your app is told neither.
165
+
166
+ ## Values the app reads
167
+
168
+ Your app sometimes has to include a value in the request it sends, such as
169
+ an account id. Mark that field `readable` in the manifest and Brass returns
170
+ it to your app with the service.
171
+
172
+ Brass collects the value when somebody connects an account, and offers your app
173
+ only the accounts carrying it, so it is there whenever a call reaches your code.
174
+
175
+ The field's name is the key your app reads the value back under.
176
+
177
+ ```json
178
+ {
179
+ "services": [
180
+ {
181
+ "host": "api.cloudflare.com",
182
+ "headers": { "authorization": "Bearer {token}" },
183
+ "fields": [{ "name": "account_id", "readable": true, "label": "Account ID" }]
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ ```js
190
+ const services = await Brass.listServices();
191
+ const cf = services?.find((s) => s.host === 'api.cloudflare.com');
192
+ const account = cf?.values?.account_id;
193
+ if (cf?.ready && account) {
194
+ const res = await Brass.callService(
195
+ 'api.cloudflare.com',
196
+ `client/v4/accounts/${account}/tokens/verify`,
197
+ { method: 'GET' },
198
+ );
199
+ }
200
+ ```
201
+
202
+ `values` is present on a `ready` service.
203
+
204
+ ## Providing credential setup instructions
205
+
206
+ Brass shows your setup instructions on the form that collects a credential,
207
+ and you are strongly encouraged to provide them. Without them the form shows
208
+ your field labels alone, and the person filling it in has to work out what
209
+ to create at the vendor and where.
210
+
211
+ Declare a `setup` beside the host:
212
+
213
+ ```json
214
+ {
215
+ "services": [
216
+ {
217
+ "host": "api.example.com",
218
+ "headers": { "authorization": "Bearer {secret}" },
219
+ "setup": {
220
+ "credentialName": "API key",
221
+ "consoleUrl": "https://example.com/settings/keys",
222
+ "docsUrl": "https://example.com/docs/api",
223
+ "steps": [
224
+ "Open Settings, then API keys.",
225
+ "Create a key with read and write access.",
226
+ "Copy the key before closing the dialog."
227
+ ]
228
+ }
229
+ }
230
+ ]
231
+ }
232
+ ```
233
+
234
+ - **`credentialName`** is what the vendor calls the credential, so it
235
+ matches what they see there.
236
+ - **`consoleUrl`** is the page where they create it. Link to that page
237
+ rather than the vendor's home page.
238
+ - **`docsUrl`** is where the vendor documents this method.
239
+ - **`steps`** are step-by-step instructions for creating the credential.
240
+ Up to six, shown in order.
241
+
242
+ ## A service split across addresses
243
+
244
+ A wildcard host (`*.myshopify.com`) covers every address under it;
245
+ [`app-manifest.md`](./app-manifest.md) covers declaring one. Which `host` you
246
+ pass to `callService` depends on `credentialPerHost`.
247
+
248
+ Without it, one credential covers every address under the suffix, and
249
+ `listServices` returns a single entry under the declared name. Match on the
250
+ wildcard, not on the address you are about to call.
251
+
252
+ With it, each secret is entered against a specific address, and
253
+ `listServices` returns one entry per connected address, each with
254
+ `declared_as: '*.myshopify.com'`. Pass the `host` from one of those entries.
255
+ An address you build from a shop name points at a shop nobody has connected.
256
+ Before any address is connected there is one entry for the wildcard itself,
257
+ and `Brass.connectService` takes that `host`:
258
+
259
+ ```js
260
+ const shops = ((await Brass.listServices()) ?? []).filter(
261
+ (s) => s.declared_as === '*.myshopify.com',
262
+ );
263
+ const connected = shops.filter((s) => s.ready);
264
+ if (connected.length === 0) await Brass.connectService('*.myshopify.com');
265
+ ```