@brass-build/client 0.3.0 → 0.4.1

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 (128) hide show
  1. package/AGENTS.md +50 -7
  2. package/CHANGELOG.md +63 -2
  3. package/README.md +233 -77
  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 +273 -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 +31 -0
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +611 -12
  17. package/dist/client.js.map +1 -1
  18. package/dist/document-contents.d.ts +15 -0
  19. package/dist/document-contents.d.ts.map +1 -0
  20. package/dist/document-contents.js +88 -0
  21. package/dist/document-contents.js.map +1 -0
  22. package/dist/document-streams.d.ts +4 -2
  23. package/dist/document-streams.d.ts.map +1 -1
  24. package/dist/document-streams.js +6 -0
  25. package/dist/document-streams.js.map +1 -1
  26. package/dist/documents.d.ts +45 -1
  27. package/dist/documents.d.ts.map +1 -1
  28. package/dist/documents.js +305 -22
  29. package/dist/documents.js.map +1 -1
  30. package/dist/export-guest.d.ts.map +1 -1
  31. package/dist/export-guest.js +5 -0
  32. package/dist/export-guest.js.map +1 -1
  33. package/dist/handle-types.d.ts +23 -0
  34. package/dist/handle-types.d.ts.map +1 -1
  35. package/dist/handles.d.ts +1 -1
  36. package/dist/handles.d.ts.map +1 -1
  37. package/dist/handles.js +1 -1
  38. package/dist/handles.js.map +1 -1
  39. package/dist/hosted-ui.d.ts +10 -0
  40. package/dist/hosted-ui.d.ts.map +1 -1
  41. package/dist/hosted-ui.js +115 -30
  42. package/dist/hosted-ui.js.map +1 -1
  43. package/dist/importer.d.ts.map +1 -1
  44. package/dist/importer.js +17 -6
  45. package/dist/importer.js.map +1 -1
  46. package/dist/index.d.ts +8 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +25 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal.d.ts +1 -0
  51. package/dist/internal.d.ts.map +1 -1
  52. package/dist/internal.js +1 -0
  53. package/dist/internal.js.map +1 -1
  54. package/dist/mutate-batching.js +2 -2
  55. package/dist/mutate-batching.js.map +1 -1
  56. package/dist/open-document.d.ts +1 -1
  57. package/dist/open-document.d.ts.map +1 -1
  58. package/dist/open-document.js +69 -34
  59. package/dist/open-document.js.map +1 -1
  60. package/dist/presence.d.ts +10 -0
  61. package/dist/presence.d.ts.map +1 -1
  62. package/dist/presence.js.map +1 -1
  63. package/dist/session-toolbar.d.ts +9 -0
  64. package/dist/session-toolbar.d.ts.map +1 -1
  65. package/dist/session-toolbar.js +60 -12
  66. package/dist/session-toolbar.js.map +1 -1
  67. package/dist/tail-ops.js +2 -2
  68. package/dist/tail-ops.js.map +1 -1
  69. package/dist/types.d.ts +0 -42
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/undo.d.ts +1 -1
  72. package/dist/undo.d.ts.map +1 -1
  73. package/dist/undo.js +1 -1
  74. package/dist/undo.js.map +1 -1
  75. package/dist/version-bar.d.ts.map +1 -1
  76. package/dist/version-bar.js +52 -2
  77. package/dist/version-bar.js.map +1 -1
  78. package/dist/version.d.ts +2 -2
  79. package/dist/version.js +1 -1
  80. package/dist/wasm-ready-gate.d.ts +13 -0
  81. package/dist/wasm-ready-gate.d.ts.map +1 -0
  82. package/dist/wasm-ready-gate.js +28 -0
  83. package/dist/wasm-ready-gate.js.map +1 -0
  84. package/dist/wasm-runtime.browser.d.ts +11 -0
  85. package/dist/wasm-runtime.browser.d.ts.map +1 -0
  86. package/dist/wasm-runtime.browser.js +44 -0
  87. package/dist/wasm-runtime.browser.js.map +1 -0
  88. package/dist/wasm-runtime.d.ts +6 -0
  89. package/dist/wasm-runtime.d.ts.map +1 -0
  90. package/dist/wasm-runtime.js +20 -0
  91. package/dist/wasm-runtime.js.map +1 -0
  92. package/docs/app-manifest.md +335 -5
  93. package/docs/custom-chrome.md +3 -0
  94. package/docs/presence.md +12 -0
  95. package/docs/services.md +265 -0
  96. package/docs/text-fields.md +191 -0
  97. package/package.json +4 -5
  98. package/src/acting-organization.ts +390 -0
  99. package/src/api-headers.ts +7 -7
  100. package/src/call-service.ts +354 -0
  101. package/src/client.ts +680 -14
  102. package/src/document-contents.ts +104 -0
  103. package/src/document-streams.ts +15 -1
  104. package/src/documents.ts +340 -20
  105. package/src/export-guest.ts +5 -0
  106. package/src/handle-types.ts +35 -0
  107. package/src/handles.ts +1 -1
  108. package/src/hosted-ui.ts +155 -29
  109. package/src/importer.ts +17 -6
  110. package/src/index.ts +43 -4
  111. package/src/internal.ts +4 -0
  112. package/src/mutate-batching.ts +2 -2
  113. package/src/open-document.ts +79 -38
  114. package/src/presence.ts +6 -0
  115. package/src/session-toolbar.ts +80 -12
  116. package/src/tail-ops.ts +2 -2
  117. package/src/types.ts +6 -4
  118. package/src/undo.ts +1 -1
  119. package/src/version-bar.ts +53 -2
  120. package/src/version.ts +1 -1
  121. package/src/wasm-ready-gate.ts +30 -0
  122. package/src/wasm-runtime.browser.ts +48 -0
  123. package/src/wasm-runtime.ts +21 -0
  124. package/dist/vite.d.ts +0 -31
  125. package/dist/vite.d.ts.map +0 -1
  126. package/dist/vite.js +0 -161
  127. package/dist/vite.js.map +0 -1
  128. package/src/vite.ts +0 -214
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Build a function that runs `inits` once and hands every caller the same
3
+ * result.
4
+ *
5
+ * A rejection is NOT retained. Each init is a multi-megabyte fetch over
6
+ * whatever connection the reader has, and a page navigating away mid-download
7
+ * aborts it, so a retained rejection would answer every later call with a
8
+ * failure the network has long since recovered from. Dropping it makes the
9
+ * next call start over, which is what a reader who opens a document a minute
10
+ * later needs.
11
+ */
12
+ export declare function createReadyGate(inits: readonly (() => Promise<unknown>)[]): () => Promise<void>;
13
+ //# sourceMappingURL=wasm-ready-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-ready-gate.d.ts","sourceRoot":"","sources":["../src/wasm-ready-gate.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,GACzC,MAAM,OAAO,CAAC,IAAI,CAAC,CAWrB"}
@@ -0,0 +1,28 @@
1
+ // The once-only gate the browser runtime half readies its WASM behind, kept
2
+ // separate from it so it can be exercised without a real engine: the half
3
+ // itself names a WASM-shipping dependency, which pins it to an environment
4
+ // that can load one.
5
+ /**
6
+ * Build a function that runs `inits` once and hands every caller the same
7
+ * result.
8
+ *
9
+ * A rejection is NOT retained. Each init is a multi-megabyte fetch over
10
+ * whatever connection the reader has, and a page navigating away mid-download
11
+ * aborts it, so a retained rejection would answer every later call with a
12
+ * failure the network has long since recovered from. Dropping it makes the
13
+ * next call start over, which is what a reader who opens a document a minute
14
+ * later needs.
15
+ */
16
+ export function createReadyGate(inits) {
17
+ let readying = null;
18
+ return () => {
19
+ readying ??= Promise.all(inits.map((init) => init()))
20
+ .then(() => undefined)
21
+ .catch((err) => {
22
+ readying = null;
23
+ throw err;
24
+ });
25
+ return readying;
26
+ };
27
+ }
28
+ //# sourceMappingURL=wasm-ready-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-ready-gate.js","sourceRoot":"","sources":["../src/wasm-ready-gate.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,qBAAqB;AAErB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAC7B,KAA0C;IAE1C,IAAI,QAAQ,GAAyB,IAAI,CAAC;IAC1C,OAAO,GAAG,EAAE;QACV,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;aAClD,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;aACrB,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,GAAG,CAAC;QACZ,CAAC,CAAC,CAAC;QACL,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export * from 'loro-crdt/web';
2
+ /**
3
+ * Resolve once every WASM module this package needs is ready to run.
4
+ *
5
+ * `Brass.init()` starts it and every path that constructs a document awaits
6
+ * it, so an app that follows the documented boot never calls it. It is
7
+ * idempotent and concurrent-safe for the paths that construct documents
8
+ * without one (a test, an in-repo tool).
9
+ */
10
+ export declare const ensureWasmReady: () => Promise<void>;
11
+ //# sourceMappingURL=wasm-runtime.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-runtime.browser.d.ts","sourceRoot":"","sources":["../src/wasm-runtime.browser.ts"],"names":[],"mappings":"AA8BA,cAAc,eAAe,CAAC;AAS9B;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAA0B,CAAC"}
@@ -0,0 +1,44 @@
1
+ // The browser half of the WASM runtime seam: the ONE module in this package
2
+ // that names a WASM-shipping dependency, and the only place a new one is
3
+ // added. Everything else imports `./wasm-runtime.js`, whose node half this
4
+ // one replaces in a browser build, so no other file in the SDK (and no line of
5
+ // an app's own config) names a WASM dep or its loader.
6
+ //
7
+ // Each entry is loaded through its package's WEB build, which readies its
8
+ // module asynchronously behind an explicit init and locates its bytes with
9
+ // `new URL(..., import.meta.url)`. That combination is what makes the whole
10
+ // package build with no bundler plugins: every major bundler rewrites that URL
11
+ // into an emitted asset on its own, and nothing here emits a top-level await,
12
+ // so no build target has to accept one.
13
+ //
14
+ // The alternative each package offers by default is worse in a way nothing
15
+ // reports. A default browser entry fetches its module with a SYNCHRONOUS
16
+ // XMLHttpRequest while it evaluates, so an app freezes its main thread for the
17
+ // whole download before it paints, whether or not anyone opens a document, and
18
+ // the freeze grows with the reader's latency rather than with the app. That is
19
+ // what a browser build resolves to unless something selects otherwise, it
20
+ // builds clean, and the only tell is the chunk count.
21
+ // The init comes from the deeper path because the package's web entry
22
+ // re-exports its module with `export *`, which does not carry a default, so
23
+ // the entry's types do not declare the init its JavaScript exports. The two
24
+ // specifiers resolve to the same module instance (the entry imports this
25
+ // file), so readying through one readies the classes taken from the other.
26
+ import initEngine from 'loro-crdt/web/loro_wasm.js';
27
+ import { createReadyGate } from './wasm-ready-gate.js';
28
+ export * from 'loro-crdt/web';
29
+ // Every WASM module this package needs ready before it runs, as the init each
30
+ // one exports. Add an entry rather than replacing the list: a consumer with
31
+ // two installed during a migration window needs both readied, and the gate
32
+ // below is what every caller already awaits, so a new entry costs no new call
33
+ // anywhere.
34
+ const INITS = [initEngine];
35
+ /**
36
+ * Resolve once every WASM module this package needs is ready to run.
37
+ *
38
+ * `Brass.init()` starts it and every path that constructs a document awaits
39
+ * it, so an app that follows the documented boot never calls it. It is
40
+ * idempotent and concurrent-safe for the paths that construct documents
41
+ * without one (a test, an in-repo tool).
42
+ */
43
+ export const ensureWasmReady = createReadyGate(INITS);
44
+ //# sourceMappingURL=wasm-runtime.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-runtime.browser.js","sourceRoot":"","sources":["../src/wasm-runtime.browser.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,+EAA+E;AAC/E,uDAAuD;AACvD,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,4EAA4E;AAC5E,+EAA+E;AAC/E,8EAA8E;AAC9E,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,0EAA0E;AAC1E,sDAAsD;AAEtD,sEAAsE;AACtE,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,OAAO,UAAU,MAAM,4BAA4B,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,cAAc,eAAe,CAAC;AAE9B,8EAA8E;AAC9E,4EAA4E;AAC5E,2EAA2E;AAC3E,8EAA8E;AAC9E,YAAY;AACZ,MAAM,KAAK,GAAwC,CAAC,UAAU,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAwB,eAAe,CAAC,KAAK,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from 'loro-crdt';
2
+ /** Resolve once every WASM module this package needs is ready to run. Already
3
+ * true here: a node build readies its module during import, so the callers
4
+ * that await it on the browser half wait for nothing on this one. */
5
+ export declare function ensureWasmReady(): Promise<void>;
6
+ //# sourceMappingURL=wasm-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-runtime.d.ts","sourceRoot":"","sources":["../src/wasm-runtime.ts"],"names":[],"mappings":"AAaA,cAAc,WAAW,CAAC;AAE1B;;sEAEsE;AACtE,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C"}
@@ -0,0 +1,20 @@
1
+ // The node half of the WASM runtime seam. Node resolves each WASM-shipping
2
+ // dependency to its own node build, which readies its module as it is
3
+ // imported, so there is nothing to await and `ensureWasmReady` is the same
4
+ // call answering immediately.
5
+ //
6
+ // This is the half every importer names, so a build that applies no
7
+ // substitution at all (tsc, a node-targeting esbuild, the test runner) is
8
+ // already correct, and the browser half's `import.meta.url` asset resolution
9
+ // never reaches a bundler targeting node. `package.json`'s `browser` map
10
+ // swaps in the browser half for a bundler building this package for one, and
11
+ // `ensureWasmReady` exists so a caller awaits readiness the same way whichever
12
+ // half it got.
13
+ export * from 'loro-crdt';
14
+ /** Resolve once every WASM module this package needs is ready to run. Already
15
+ * true here: a node build readies its module during import, so the callers
16
+ * that await it on the browser half wait for nothing on this one. */
17
+ export function ensureWasmReady() {
18
+ return Promise.resolve();
19
+ }
20
+ //# sourceMappingURL=wasm-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-runtime.js","sourceRoot":"","sources":["../src/wasm-runtime.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,sEAAsE;AACtE,2EAA2E;AAC3E,8BAA8B;AAC9B,EAAE;AACF,oEAAoE;AACpE,0EAA0E;AAC1E,6EAA6E;AAC7E,yEAAyE;AACzE,6EAA6E;AAC7E,+EAA+E;AAC/E,eAAe;AAEf,cAAc,WAAW,CAAC;AAE1B;;sEAEsE;AACtE,MAAM,UAAU,eAAe;IAC7B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;AAC3B,CAAC"}
@@ -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.
@@ -130,6 +130,9 @@ hook for keyboard shortcuts).
130
130
  Reading the list and calling `run()` wires all of it. You render and
131
131
  invoke, and the SDK owns the behavior:
132
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.
133
136
  - **`history`** drives version preview end to end. During a preview
134
137
  `doc.readOnly` is `true`, so gate your own edit controls on it;
135
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: