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