@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.
- package/AGENTS.md +50 -7
- package/CHANGELOG.md +63 -2
- package/README.md +233 -77
- package/dist/acting-organization.d.ts +131 -0
- package/dist/acting-organization.d.ts.map +1 -0
- package/dist/acting-organization.js +273 -0
- package/dist/acting-organization.js.map +1 -0
- package/dist/api-headers.js +7 -7
- package/dist/api-headers.js.map +1 -1
- package/dist/call-service.d.ts +74 -0
- package/dist/call-service.d.ts.map +1 -0
- package/dist/call-service.js +253 -0
- package/dist/call-service.js.map +1 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +611 -12
- package/dist/client.js.map +1 -1
- package/dist/document-contents.d.ts +15 -0
- package/dist/document-contents.d.ts.map +1 -0
- package/dist/document-contents.js +88 -0
- package/dist/document-contents.js.map +1 -0
- package/dist/document-streams.d.ts +4 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +6 -0
- package/dist/document-streams.js.map +1 -1
- package/dist/documents.d.ts +45 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +305 -22
- package/dist/documents.js.map +1 -1
- package/dist/export-guest.d.ts.map +1 -1
- package/dist/export-guest.js +5 -0
- package/dist/export-guest.js.map +1 -1
- package/dist/handle-types.d.ts +23 -0
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +1 -1
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +1 -1
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +10 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +115 -30
- package/dist/hosted-ui.js.map +1 -1
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +17 -6
- package/dist/importer.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/mutate-batching.js +2 -2
- package/dist/mutate-batching.js.map +1 -1
- package/dist/open-document.d.ts +1 -1
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +69 -34
- package/dist/open-document.js.map +1 -1
- package/dist/presence.d.ts +10 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js.map +1 -1
- package/dist/session-toolbar.d.ts +9 -0
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +60 -12
- package/dist/session-toolbar.js.map +1 -1
- package/dist/tail-ops.js +2 -2
- package/dist/tail-ops.js.map +1 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +1 -1
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +1 -1
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +52 -2
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/wasm-ready-gate.d.ts +13 -0
- package/dist/wasm-ready-gate.d.ts.map +1 -0
- package/dist/wasm-ready-gate.js +28 -0
- package/dist/wasm-ready-gate.js.map +1 -0
- package/dist/wasm-runtime.browser.d.ts +11 -0
- package/dist/wasm-runtime.browser.d.ts.map +1 -0
- package/dist/wasm-runtime.browser.js +44 -0
- package/dist/wasm-runtime.browser.js.map +1 -0
- package/dist/wasm-runtime.d.ts +6 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +20 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/docs/app-manifest.md +335 -5
- package/docs/custom-chrome.md +3 -0
- package/docs/presence.md +12 -0
- package/docs/services.md +265 -0
- package/docs/text-fields.md +191 -0
- package/package.json +4 -5
- package/src/acting-organization.ts +390 -0
- package/src/api-headers.ts +7 -7
- package/src/call-service.ts +354 -0
- package/src/client.ts +680 -14
- package/src/document-contents.ts +104 -0
- package/src/document-streams.ts +15 -1
- package/src/documents.ts +340 -20
- package/src/export-guest.ts +5 -0
- package/src/handle-types.ts +35 -0
- package/src/handles.ts +1 -1
- package/src/hosted-ui.ts +155 -29
- package/src/importer.ts +17 -6
- package/src/index.ts +43 -4
- package/src/internal.ts +4 -0
- package/src/mutate-batching.ts +2 -2
- package/src/open-document.ts +79 -38
- package/src/presence.ts +6 -0
- package/src/session-toolbar.ts +80 -12
- package/src/tail-ops.ts +2 -2
- package/src/types.ts +6 -4
- package/src/undo.ts +1 -1
- package/src/version-bar.ts +53 -2
- package/src/version.ts +1 -1
- package/src/wasm-ready-gate.ts +30 -0
- package/src/wasm-runtime.browser.ts +48 -0
- package/src/wasm-runtime.ts +21 -0
- package/dist/vite.d.ts +0 -31
- package/dist/vite.d.ts.map +0 -1
- package/dist/vite.js +0 -161
- package/dist/vite.js.map +0 -1
- 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"}
|
package/docs/app-manifest.md
CHANGED
|
@@ -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`.
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
"
|
|
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
|
|
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.
|
package/docs/custom-chrome.md
CHANGED
|
@@ -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:
|