@brass-build/client 0.4.1 → 0.5.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 +124 -45
- package/CHANGELOG.md +80 -4
- package/README.md +666 -179
- package/dist/api-fetch.d.ts +8 -0
- package/dist/api-fetch.d.ts.map +1 -1
- package/dist/api-fetch.js +35 -8
- package/dist/api-fetch.js.map +1 -1
- package/dist/batched-get.d.ts +11 -0
- package/dist/batched-get.d.ts.map +1 -0
- package/dist/batched-get.js +92 -0
- package/dist/batched-get.js.map +1 -0
- package/dist/call-service.d.ts +4 -0
- package/dist/call-service.d.ts.map +1 -1
- package/dist/call-service.js +66 -3
- package/dist/call-service.js.map +1 -1
- package/dist/client.d.ts +48 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +918 -115
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +25 -2
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +258 -61
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-commands.d.ts.map +1 -1
- package/dist/document-commands.js +5 -3
- package/dist/document-commands.js.map +1 -1
- package/dist/document-contents.d.ts +10 -0
- package/dist/document-contents.d.ts.map +1 -1
- package/dist/document-contents.js +23 -1
- package/dist/document-contents.js.map +1 -1
- package/dist/document-set-subscription.d.ts +56 -0
- package/dist/document-set-subscription.d.ts.map +1 -0
- package/dist/document-set-subscription.js +240 -0
- package/dist/document-set-subscription.js.map +1 -0
- package/dist/document-signals.d.ts +6 -0
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +31 -1
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +36 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +69 -18
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +12 -2
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +103 -27
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts +92 -10
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +285 -39
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +173 -8
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1778 -310
- package/dist/documents.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch-retry.d.ts +1 -0
- package/dist/fetch-retry.d.ts.map +1 -1
- package/dist/fetch-retry.js +2 -0
- package/dist/fetch-retry.js.map +1 -1
- package/dist/file-choice.d.ts +2 -0
- package/dist/file-choice.d.ts.map +1 -0
- package/dist/file-choice.js +27 -0
- package/dist/file-choice.js.map +1 -0
- package/dist/guest-link.d.ts +18 -1
- package/dist/guest-link.d.ts.map +1 -1
- package/dist/guest-link.js +329 -1
- package/dist/guest-link.js.map +1 -1
- package/dist/handle-types.d.ts +67 -5
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +15 -19
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +231 -76
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +61 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +94 -42
- package/dist/hosted-ui.js.map +1 -1
- package/dist/import-contract.d.ts +74 -0
- package/dist/import-contract.d.ts.map +1 -0
- package/dist/import-contract.js +163 -0
- package/dist/import-contract.js.map +1 -0
- package/dist/import-guest.d.ts +9 -0
- package/dist/import-guest.d.ts.map +1 -0
- package/dist/import-guest.js +155 -0
- package/dist/import-guest.js.map +1 -0
- package/dist/import-report.d.ts +17 -0
- package/dist/import-report.d.ts.map +1 -0
- package/dist/import-report.js +7 -0
- package/dist/import-report.js.map +1 -0
- package/dist/importer-wait.d.ts +2 -0
- package/dist/importer-wait.d.ts.map +1 -0
- package/dist/importer-wait.js +5 -0
- package/dist/importer-wait.js.map +1 -0
- package/dist/importer.d.ts +18 -4
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +322 -105
- package/dist/importer.js.map +1 -1
- package/dist/in-development.d.ts +2 -0
- package/dist/in-development.d.ts.map +1 -0
- package/dist/in-development.js +10 -0
- package/dist/in-development.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/init-keys.d.ts +1 -1
- package/dist/init-keys.d.ts.map +1 -1
- package/dist/init-keys.js +6 -1
- package/dist/init-keys.js.map +1 -1
- package/dist/input-documents.d.ts +67 -0
- package/dist/input-documents.d.ts.map +1 -0
- package/dist/input-documents.js +693 -0
- package/dist/input-documents.js.map +1 -0
- package/dist/inputs-status.d.ts +80 -0
- package/dist/inputs-status.d.ts.map +1 -0
- package/dist/inputs-status.js +460 -0
- package/dist/inputs-status.js.map +1 -0
- package/dist/inputs.d.ts +2 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +793 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/internal.js.map +1 -1
- package/dist/join-content.d.ts +33 -0
- package/dist/join-content.d.ts.map +1 -0
- package/dist/join-content.js +114 -0
- package/dist/join-content.js.map +1 -0
- package/dist/join-stamp.d.ts +3 -0
- package/dist/join-stamp.d.ts.map +1 -0
- package/dist/join-stamp.js +12 -0
- package/dist/join-stamp.js.map +1 -0
- package/dist/manifest-drift.d.ts +9 -0
- package/dist/manifest-drift.d.ts.map +1 -0
- package/dist/manifest-drift.js +46 -0
- package/dist/manifest-drift.js.map +1 -0
- package/dist/open-document.d.ts +109 -2
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +1822 -811
- package/dist/open-document.js.map +1 -1
- package/dist/page-restore.d.ts +12 -0
- package/dist/page-restore.d.ts.map +1 -0
- package/dist/page-restore.js +23 -0
- package/dist/page-restore.js.map +1 -0
- package/dist/placement.d.ts +13 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +34 -0
- package/dist/placement.js.map +1 -0
- package/dist/presence.d.ts +1 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +16 -4
- package/dist/presence.js.map +1 -1
- package/dist/read-only-document.d.ts +87 -0
- package/dist/read-only-document.d.ts.map +1 -0
- package/dist/read-only-document.js +4 -0
- package/dist/read-only-document.js.map +1 -0
- package/dist/record-paging.d.ts +63 -6
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +122 -29
- package/dist/record-paging.js.map +1 -1
- package/dist/references.d.ts +40 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +135 -0
- package/dist/references.js.map +1 -0
- package/dist/request-lane.d.ts +5 -0
- package/dist/request-lane.d.ts.map +1 -0
- package/dist/request-lane.js +48 -0
- package/dist/request-lane.js.map +1 -0
- package/dist/sealed-arrow.d.ts +170 -0
- package/dist/sealed-arrow.d.ts.map +1 -0
- package/dist/sealed-arrow.js +769 -0
- package/dist/sealed-arrow.js.map +1 -0
- package/dist/sealed-cache.d.ts +21 -0
- package/dist/sealed-cache.d.ts.map +1 -0
- package/dist/sealed-cache.js +247 -0
- package/dist/sealed-cache.js.map +1 -0
- package/dist/sealed-encode.d.ts +87 -0
- package/dist/sealed-encode.d.ts.map +1 -0
- package/dist/sealed-encode.js +876 -0
- package/dist/sealed-encode.js.map +1 -0
- package/dist/sealed-handles.d.ts +45 -0
- package/dist/sealed-handles.d.ts.map +1 -0
- package/dist/sealed-handles.js +408 -0
- package/dist/sealed-handles.js.map +1 -0
- package/dist/sealed-store.d.ts +242 -0
- package/dist/sealed-store.d.ts.map +1 -0
- package/dist/sealed-store.js +1155 -0
- package/dist/sealed-store.js.map +1 -0
- package/dist/serializer.d.ts +6 -15
- package/dist/serializer.d.ts.map +1 -1
- package/dist/session-toolbar.d.ts +20 -5
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +419 -67
- package/dist/session-toolbar.js.map +1 -1
- package/dist/session.d.ts +38 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -79
- package/dist/session.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +74 -0
- package/dist/sha256.js.map +1 -0
- package/dist/stable-handles.d.ts +19 -0
- package/dist/stable-handles.d.ts.map +1 -0
- package/dist/stable-handles.js +193 -0
- package/dist/stable-handles.js.map +1 -0
- package/dist/staged-seal.d.ts +2 -0
- package/dist/staged-seal.d.ts.map +1 -0
- package/dist/staged-seal.js +76 -0
- package/dist/staged-seal.js.map +1 -0
- package/dist/subscriber-delivery.d.ts +8 -0
- package/dist/subscriber-delivery.d.ts.map +1 -0
- package/dist/subscriber-delivery.js +31 -0
- package/dist/subscriber-delivery.js.map +1 -0
- package/dist/tail-op-import.d.ts +10 -0
- package/dist/tail-op-import.d.ts.map +1 -0
- package/dist/tail-op-import.js +33 -0
- package/dist/tail-op-import.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +6 -0
- package/dist/topic-lifecycle.d.ts.map +1 -1
- package/dist/topic-lifecycle.js +32 -2
- package/dist/topic-lifecycle.js.map +1 -1
- package/dist/trial-replace-dialog.d.ts +10 -0
- package/dist/trial-replace-dialog.d.ts.map +1 -0
- package/dist/trial-replace-dialog.js +153 -0
- package/dist/trial-replace-dialog.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +60 -5
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +344 -75
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts +1 -0
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +2 -1
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +21 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +192 -8
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts +1 -0
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +121 -17
- package/docs/custom-chrome.md +24 -16
- package/docs/hosted-ui-advanced.md +38 -6
- package/docs/imported-documents.md +104 -51
- package/docs/presence.md +16 -4
- package/docs/schemas.md +8 -5
- package/docs/services.md +6 -6
- package/docs/streams.md +139 -150
- package/docs/text-fields.md +26 -19
- package/package.json +5 -2
- package/src/api-fetch.ts +53 -3
- package/src/auth-flow.ts +1 -1
- package/src/batched-get.ts +117 -0
- package/src/call-service.ts +78 -3
- package/src/client.ts +1022 -112
- package/src/document-checkpoints.ts +307 -65
- package/src/document-commands.ts +5 -5
- package/src/document-contents.ts +27 -1
- package/src/document-set-subscription.ts +274 -0
- package/src/document-signals.ts +47 -5
- package/src/document-streams.ts +85 -16
- package/src/document-thumbnail-capture.ts +118 -32
- package/src/documents-namespace.ts +539 -84
- package/src/documents.ts +2017 -348
- package/src/errors.ts +17 -6
- package/src/fetch-retry.ts +5 -0
- package/src/file-choice.ts +26 -0
- package/src/guest-link.ts +362 -1
- package/src/handle-types.ts +73 -47
- package/src/handles.ts +253 -83
- package/src/hosted-ui.ts +211 -50
- package/src/import-contract.ts +227 -0
- package/src/import-guest.ts +193 -0
- package/src/import-report.ts +24 -0
- package/src/importer-wait.ts +4 -0
- package/src/importer.ts +386 -131
- package/src/in-development.ts +10 -0
- package/src/index.ts +76 -16
- package/src/init-keys.ts +6 -1
- package/src/input-documents.ts +819 -0
- package/src/inputs-status.ts +598 -0
- package/src/inputs.ts +1090 -0
- package/src/internal.ts +7 -0
- package/src/join-content.ts +135 -0
- package/src/join-stamp.ts +12 -0
- package/src/manifest-drift.ts +54 -0
- package/src/open-document.ts +2346 -930
- package/src/page-restore.ts +38 -0
- package/src/placement.ts +54 -0
- package/src/presence.ts +21 -5
- package/src/read-only-document.ts +133 -0
- package/src/record-paging.ts +147 -25
- package/src/references.ts +207 -0
- package/src/request-lane.ts +59 -0
- package/src/sealed-arrow.ts +843 -0
- package/src/sealed-cache.ts +267 -0
- package/src/sealed-encode.ts +1015 -0
- package/src/sealed-handles.ts +486 -0
- package/src/sealed-store.ts +1330 -0
- package/src/serializer.ts +8 -20
- package/src/session-toolbar.ts +463 -72
- package/src/session.ts +740 -99
- package/src/sha256.ts +65 -0
- package/src/stable-handles.ts +235 -0
- package/src/staged-seal.ts +90 -0
- package/src/subscriber-delivery.ts +37 -0
- package/src/tail-op-import.ts +30 -0
- package/src/topic-lifecycle.ts +38 -2
- package/src/trial-replace-dialog.ts +163 -0
- package/src/types.ts +6 -6
- package/src/undo.ts +386 -73
- package/src/version-bar.ts +5 -1
- package/src/version.ts +1 -1
- package/src/ws-client.ts +208 -16
- package/src/ws-frame-limit.ts +1 -0
package/src/hosted-ui.ts
CHANGED
|
@@ -101,6 +101,98 @@ export interface SelectDocumentResult {
|
|
|
101
101
|
branch_id?: string;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** What an app passes to `Brass.chooseInputs`. */
|
|
105
|
+
export interface ChooseInputsOptions {
|
|
106
|
+
/** The references this app already holds. The picker opens with them
|
|
107
|
+
* ticked and resolves with the whole set, so replace what you kept with
|
|
108
|
+
* the result rather than appending to it. */
|
|
109
|
+
selected?: readonly InputReference[];
|
|
110
|
+
/** Narrows the files of those families by name or extension. */
|
|
111
|
+
accept?: SelectionNarrowing;
|
|
112
|
+
/** One line naming what the app is asking for. */
|
|
113
|
+
prompt?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What an app passes to `Brass.chooseInput`. */
|
|
117
|
+
export interface ChooseInputOptions {
|
|
118
|
+
/** The reference this app already holds for this choice. The picker opens
|
|
119
|
+
* with that file marked. */
|
|
120
|
+
selected?: InputReference | null;
|
|
121
|
+
/** Narrows the files of those families by name or extension. */
|
|
122
|
+
accept?: SelectionNarrowing;
|
|
123
|
+
/** One line naming what the app is asking for. */
|
|
124
|
+
prompt?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One file, folder, project or organization a person chose for this app,
|
|
129
|
+
* which `doc.dependsOn` records as a reference. A place (`organization`,
|
|
130
|
+
* `project`, or a folder named by `doc_id`) keeps answering, so a document
|
|
131
|
+
* filed there later is included; a file is that one document.
|
|
132
|
+
*/
|
|
133
|
+
export interface InputReference {
|
|
134
|
+
organization_id: string;
|
|
135
|
+
/** `document` names a file or a folder by `doc_id`. */
|
|
136
|
+
extent: 'organization' | 'project' | 'document';
|
|
137
|
+
project_id?: string;
|
|
138
|
+
doc_id?: string;
|
|
139
|
+
/** The name the picker showed for this entry when the reader chose it. It
|
|
140
|
+
* does not follow a later rename; choosing again returns the new name. */
|
|
141
|
+
label?: string;
|
|
142
|
+
/** What the picker a folder, project or organization was chosen in
|
|
143
|
+
* offered. Listing the reference includes only the documents it admits, so
|
|
144
|
+
* a place chosen in two pickers stays apart. Keep it with the reference. */
|
|
145
|
+
accept?: SelectionAccept;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The families a picker offers, as your manifest's `opens` and `inputs`
|
|
149
|
+
* declare them. A family your manifest does not declare offers nothing. */
|
|
150
|
+
export type DocumentFamilies = readonly string[];
|
|
151
|
+
|
|
152
|
+
/** Narrows the files of the families a request offers. A file passes when
|
|
153
|
+
* it matches a name or an extension given. */
|
|
154
|
+
export interface SelectionNarrowing {
|
|
155
|
+
/** Names to match, case-insensitively, against a document's name and the
|
|
156
|
+
* name of the file it was imported from. */
|
|
157
|
+
names?: readonly string[];
|
|
158
|
+
/** File extensions, with or without the leading dot. */
|
|
159
|
+
extensions?: readonly string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** What a picker offered: the families it named, and any narrowing. */
|
|
163
|
+
export interface SelectionAccept extends SelectionNarrowing {
|
|
164
|
+
/** Absent when the picker offered every family. */
|
|
165
|
+
families?: readonly string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The `accept` a picker sends, from the families and narrowing an app passed.
|
|
169
|
+
// Throws when the families are missing, since without them the picker would
|
|
170
|
+
// offer everything the app can read, its own documents included.
|
|
171
|
+
function pickerAccept(
|
|
172
|
+
method: string,
|
|
173
|
+
families: unknown,
|
|
174
|
+
narrowing: SelectionNarrowing | undefined,
|
|
175
|
+
): SelectionAccept | undefined {
|
|
176
|
+
const named =
|
|
177
|
+
Array.isArray(families) &&
|
|
178
|
+
families.length > 0 &&
|
|
179
|
+
families.every((t) => typeof t === 'string' && t.trim() !== '');
|
|
180
|
+
if (families !== '*' && !named) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`${method}: pass the families to offer first, such as ['ifc']`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (narrowing !== undefined && ('families' in narrowing || 'types' in narrowing)) {
|
|
186
|
+
throw new Error(`${method}: name the families in the first argument rather than in accept`);
|
|
187
|
+
}
|
|
188
|
+
const accept: SelectionAccept = {
|
|
189
|
+
...(named ? { families: [...(families as readonly string[])] } : {}),
|
|
190
|
+
...(narrowing?.names?.length ? { names: [...narrowing.names] } : {}),
|
|
191
|
+
...(narrowing?.extensions?.length ? { extensions: [...narrowing.extensions] } : {}),
|
|
192
|
+
};
|
|
193
|
+
return Object.keys(accept).length > 0 ? accept : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
104
196
|
// Result of the hosted create-destination dialog: where a new document goes,
|
|
105
197
|
// plus the chosen name. The dialog resolves a destination only; the SDK does
|
|
106
198
|
// the create itself so the app's credential path stamps `creating_app_id` and
|
|
@@ -109,6 +201,7 @@ export interface SelectDocumentResult {
|
|
|
109
201
|
export interface CreateDestinationResult {
|
|
110
202
|
organization_id: string;
|
|
111
203
|
project_id: string | null;
|
|
204
|
+
folder_id?: string | null;
|
|
112
205
|
name: string;
|
|
113
206
|
}
|
|
114
207
|
|
|
@@ -177,9 +270,9 @@ export interface ServiceCredentialResult {
|
|
|
177
270
|
// how a `behind` / `diverged` drive-linked doc was resolved. `null` when
|
|
178
271
|
// dismissed.
|
|
179
272
|
export interface DriveResolveResult {
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
273
|
+
// A document to switch onto, or `null` when the resolution kept this one:
|
|
274
|
+
// publish-over, take-source and keep-both all resolve in place, and an open
|
|
275
|
+
// copy moves onto the drive's version when it lands.
|
|
183
276
|
newDocId: string | null;
|
|
184
277
|
}
|
|
185
278
|
|
|
@@ -208,6 +301,23 @@ export interface HostedUiLauncher {
|
|
|
208
301
|
// Iframe-only with a `BrassError` reject (a returned value can't survive a
|
|
209
302
|
// redirect fallback).
|
|
210
303
|
selectDocument(): Promise<SelectDocumentResult | null>;
|
|
304
|
+
// The same picker as a tree of tick boxes, asking which documents this
|
|
305
|
+
// app has rather than which one to open now. Ticking a folder includes
|
|
306
|
+
// what is filed there later; ticking a document is that document. What
|
|
307
|
+
// they choose is their own reach, so every later read is still clamped to
|
|
308
|
+
// what they can see. Resolves with everything currently chosen.
|
|
309
|
+
// Iframe-only, same rationale as `selectDocument`.
|
|
310
|
+
chooseInputs(
|
|
311
|
+
families: DocumentFamilies,
|
|
312
|
+
options?: ChooseInputsOptions,
|
|
313
|
+
): Promise<InputReference[] | null>;
|
|
314
|
+
// The same picker as the ordinary Open list, where one press on a file
|
|
315
|
+
// chooses it. Resolves with that file's reference, or `null` when the
|
|
316
|
+
// reader closes it.
|
|
317
|
+
chooseInput(
|
|
318
|
+
families: DocumentFamilies,
|
|
319
|
+
options?: ChooseInputOptions,
|
|
320
|
+
): Promise<InputReference | null>;
|
|
211
321
|
// The document picker opened straight on its New panel to ask where a new
|
|
212
322
|
// document goes. Used by `Brass.session.create()` when a cold create is
|
|
213
323
|
// ambiguous across multiple orgs, so the user picks rather than the SDK
|
|
@@ -271,10 +381,9 @@ export interface HostedUiLauncher {
|
|
|
271
381
|
// (default selection is the only org, or the first by membership time).
|
|
272
382
|
installApp(organizationId?: string): Promise<InstallAppResult | null>;
|
|
273
383
|
// Hosted drive-divergence resolution (from `Document.resolveDrive()`):
|
|
274
|
-
// publish over the source, take the source, or keep both
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// with the outcome or `null`. Iframe-only with a `BrassError` reject.
|
|
384
|
+
// publish over the source, take the source, or keep both, each in place
|
|
385
|
+
// (`newDocId: null`). Resolves with the outcome or `null`. Iframe-only with
|
|
386
|
+
// a `BrassError` reject.
|
|
278
387
|
driveResolve(docId: string): Promise<DriveResolveResult | null>;
|
|
279
388
|
// Hosted surface for a service credential this app is waiting on. Whoever
|
|
280
389
|
// can supply it connects the account and admits this app to it; anyone else
|
|
@@ -296,10 +405,10 @@ export interface HostedUiLauncherOptions {
|
|
|
296
405
|
// list; absent leaves it on everything the caller can reach, which is what
|
|
297
406
|
// an app that has not resolved one yet should show.
|
|
298
407
|
actingOrganizationId?: () => string | null;
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// Optional so
|
|
408
|
+
// Sent as `appId` in every surface's init. Every Brass app shares one
|
|
409
|
+
// sign-in client, so the JWT can't name the asking app; the embed page reads
|
|
410
|
+
// it off the bridge, and checks the framing page may host that app before it
|
|
411
|
+
// renders. Optional so fixtures can omit it.
|
|
303
412
|
callingAppId?: string;
|
|
304
413
|
// Pins the modal + embed page to a theme. Defaults to `'light'`; pass
|
|
305
414
|
// `'auto'` to follow OS preference. Host apps that force a theme pass it so
|
|
@@ -941,16 +1050,16 @@ export function createHostedUiLauncher(
|
|
|
941
1050
|
// would put a dark embed inside a light host on a dark-OS user.
|
|
942
1051
|
const theme: HostedUiTheme = opts.theme ?? 'light';
|
|
943
1052
|
|
|
944
|
-
// Name the
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
): Record<string, unknown> => {
|
|
1053
|
+
// Name the app and the user it acts as in every surface's bridge params.
|
|
1054
|
+
// The app id is what the embed checks the framing page against, so a
|
|
1055
|
+
// surface opened without it is one whose host goes unchecked. The acting sub
|
|
1056
|
+
// is read at open time, not launcher-construction time, since the app may
|
|
1057
|
+
// authenticate after the launcher is built.
|
|
1058
|
+
const withCaller = (params: Record<string, unknown>): Record<string, unknown> => {
|
|
951
1059
|
const actingSub = opts.getActingSub?.();
|
|
952
1060
|
return {
|
|
953
1061
|
...params,
|
|
1062
|
+
...(opts.callingAppId !== undefined ? { appId: opts.callingAppId } : {}),
|
|
954
1063
|
...(actingSub ? { actingSub } : {}),
|
|
955
1064
|
};
|
|
956
1065
|
};
|
|
@@ -959,17 +1068,17 @@ export function createHostedUiLauncher(
|
|
|
959
1068
|
// dismissal, otherwise the payload. `exportDoc` threads the export
|
|
960
1069
|
// serializer for the document surfaces whose page offers a Download
|
|
961
1070
|
// action.
|
|
962
|
-
const
|
|
1071
|
+
const openSurface = (
|
|
963
1072
|
surface: string,
|
|
964
1073
|
params: Record<string, unknown>,
|
|
965
1074
|
unavailable: UnavailableMode,
|
|
966
1075
|
exportDoc?: boolean,
|
|
967
|
-
): Promise<
|
|
968
|
-
|
|
1076
|
+
): Promise<EmbedOutcome | null> =>
|
|
1077
|
+
openEmbed(
|
|
969
1078
|
baseUrl,
|
|
970
1079
|
dashboardOrigin,
|
|
971
1080
|
surface,
|
|
972
|
-
|
|
1081
|
+
withCaller(params),
|
|
973
1082
|
unavailable,
|
|
974
1083
|
theme,
|
|
975
1084
|
exportDoc ? opts.serializeForExport : undefined,
|
|
@@ -978,25 +1087,69 @@ export function createHostedUiLauncher(
|
|
|
978
1087
|
opts.onLocalDelete,
|
|
979
1088
|
opts.onSignInRequested,
|
|
980
1089
|
);
|
|
1090
|
+
|
|
1091
|
+
const launch = async <T>(
|
|
1092
|
+
surface: string,
|
|
1093
|
+
params: Record<string, unknown>,
|
|
1094
|
+
unavailable: UnavailableMode,
|
|
1095
|
+
exportDoc?: boolean,
|
|
1096
|
+
): Promise<T | null> => {
|
|
1097
|
+
const outcome = await openSurface(surface, params, unavailable, exportDoc);
|
|
981
1098
|
if (!outcome) return null;
|
|
982
1099
|
return outcome.payload as T;
|
|
983
1100
|
};
|
|
984
1101
|
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
|
|
990
|
-
|
|
1102
|
+
// One input picker at a time. A second call while one is open (a double
|
|
1103
|
+
// press) shares it when it asks the same question, so the reader answers
|
|
1104
|
+
// once and every caller hears that answer; a different question is
|
|
1105
|
+
// refused rather than stacked behind the first. The slot clears on a
|
|
1106
|
+
// reaction registered on the dialog's own promise, ahead of any caller's,
|
|
1107
|
+
// so a call made as soon as the first settles opens a fresh picker.
|
|
1108
|
+
let inputPicker: { key: string; promise: Promise<InputReference[] | null> } | null = null;
|
|
1109
|
+
const pickInputs = (params: Record<string, unknown>): Promise<InputReference[] | null> => {
|
|
1110
|
+
const key = JSON.stringify(params);
|
|
1111
|
+
if (inputPicker) {
|
|
1112
|
+
if (inputPicker.key === key) return inputPicker.promise;
|
|
1113
|
+
return Promise.reject(new BrassError('A document picker is already open.'));
|
|
1114
|
+
}
|
|
1115
|
+
const opened = openSurface('select-document', params, 'reject');
|
|
1116
|
+
const promise = opened.then((outcome) =>
|
|
1117
|
+
outcome ? (outcome.payload as InputReference[]) : null,
|
|
1118
|
+
);
|
|
1119
|
+
const entry = { key, promise };
|
|
1120
|
+
const clear = (): void => {
|
|
1121
|
+
if (inputPicker === entry) inputPicker = null;
|
|
1122
|
+
};
|
|
1123
|
+
opened.then(clear, clear);
|
|
1124
|
+
inputPicker = entry;
|
|
1125
|
+
return promise;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// The params a choice opens the picker with. `choose` is what the picker
|
|
1129
|
+
// reads to answer with references; everything else about the surface is
|
|
1130
|
+
// the same, which is why this is one param rather than a second surface.
|
|
1131
|
+
// A choice takes no redirect: the fallback carries its answer home in
|
|
1132
|
+
// one-shot URL params built for an opened document, so a browser that
|
|
1133
|
+
// cannot host the frame rejects instead of ejecting the reader.
|
|
1134
|
+
const chooseParams = (
|
|
1135
|
+
method: string,
|
|
1136
|
+
families: DocumentFamilies,
|
|
1137
|
+
options: { accept?: SelectionNarrowing; prompt?: string } | undefined,
|
|
1138
|
+
selected: readonly InputReference[],
|
|
1139
|
+
): Record<string, unknown> => {
|
|
1140
|
+
const accept = pickerAccept(method, families, options?.accept);
|
|
1141
|
+
const params: Record<string, unknown> = { choose: true };
|
|
1142
|
+
if (selected.length) params['selected'] = [...selected];
|
|
1143
|
+
if (accept) params['accept'] = accept;
|
|
1144
|
+
if (options?.prompt !== undefined) params['prompt'] = options.prompt;
|
|
1145
|
+
const actingOrganizationId = opts.actingOrganizationId?.();
|
|
1146
|
+
if (actingOrganizationId) params['organizationId'] = actingOrganizationId;
|
|
1147
|
+
return params;
|
|
1148
|
+
};
|
|
991
1149
|
|
|
992
1150
|
return {
|
|
993
1151
|
async selectDocument(): Promise<SelectDocumentResult | null> {
|
|
994
|
-
// Thread the calling app id over the bridge (every Brass app shares one
|
|
995
|
-
// sign-in client, so the origin can't otherwise tell which
|
|
996
|
-
// app opened the picker). The embed page uses it for a "only documents
|
|
997
|
-
// this app can open" filter.
|
|
998
1152
|
const params: Record<string, unknown> = {};
|
|
999
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1000
1153
|
// Where the app is working, so the picker opens there rather than on
|
|
1001
1154
|
// every document the person can reach. Without it the first list an app
|
|
1002
1155
|
// shows spans organizations the app is not installed on, and picking
|
|
@@ -1027,6 +1180,26 @@ export function createHostedUiLauncher(
|
|
|
1027
1180
|
pickReturn ? 'redirect-with-value' : 'reject',
|
|
1028
1181
|
);
|
|
1029
1182
|
},
|
|
1183
|
+
async chooseInputs(
|
|
1184
|
+
families: DocumentFamilies,
|
|
1185
|
+
options?: ChooseInputsOptions,
|
|
1186
|
+
): Promise<InputReference[] | null> {
|
|
1187
|
+
return pickInputs(
|
|
1188
|
+
chooseParams('chooseInputs', families, options, options?.selected ?? []),
|
|
1189
|
+
);
|
|
1190
|
+
},
|
|
1191
|
+
async chooseInput(
|
|
1192
|
+
families: DocumentFamilies,
|
|
1193
|
+
options?: ChooseInputOptions,
|
|
1194
|
+
): Promise<InputReference | null> {
|
|
1195
|
+
const held = options?.selected ?? null;
|
|
1196
|
+
const params = chooseParams('chooseInput', families, options, held ? [held] : []);
|
|
1197
|
+
// `multiple: false` draws the browsing list rather than the tree, and
|
|
1198
|
+
// the picker answers with an array of the one reference.
|
|
1199
|
+
params['multiple'] = false;
|
|
1200
|
+
const chosen = await pickInputs(params);
|
|
1201
|
+
return chosen?.[0] ?? null;
|
|
1202
|
+
},
|
|
1030
1203
|
async selectCreateDestination(options?: {
|
|
1031
1204
|
name?: string;
|
|
1032
1205
|
}): Promise<CreateDestinationResult | null> {
|
|
@@ -1035,13 +1208,11 @@ export function createHostedUiLauncher(
|
|
|
1035
1208
|
// it back instead of creating the doc, so the create stays on the SDK's
|
|
1036
1209
|
// credential path. `name` pre-fills the dialog.
|
|
1037
1210
|
const params: Record<string, unknown> = { intent: 'create' };
|
|
1038
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1039
1211
|
if (options?.name !== undefined) params['name'] = options.name;
|
|
1040
1212
|
return launch<CreateDestinationResult>('select-document', params, 'reject');
|
|
1041
1213
|
},
|
|
1042
1214
|
async acquireOrganization(): Promise<AcquireOrganizationResult | null> {
|
|
1043
1215
|
const params: Record<string, unknown> = {};
|
|
1044
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1045
1216
|
return launch<AcquireOrganizationResult>(
|
|
1046
1217
|
'acquire-organization',
|
|
1047
1218
|
params,
|
|
@@ -1055,7 +1226,7 @@ export function createHostedUiLauncher(
|
|
|
1055
1226
|
// `newDocId` into `openSiblingDoc`, and this one cannot.
|
|
1056
1227
|
await launch(
|
|
1057
1228
|
'share-document',
|
|
1058
|
-
{
|
|
1229
|
+
{ docId, focus: 'share' },
|
|
1059
1230
|
'redirect',
|
|
1060
1231
|
true,
|
|
1061
1232
|
);
|
|
@@ -1079,7 +1250,7 @@ export function createHostedUiLauncher(
|
|
|
1079
1250
|
// `newDocId` into `openSiblingDoc`. The hosted page is served
|
|
1080
1251
|
// fresh against pinned SDKs, so it offers the Open only to a host
|
|
1081
1252
|
// that says it will honour it.
|
|
1082
|
-
{
|
|
1253
|
+
{ docId, focus, canOpenCopy: true },
|
|
1083
1254
|
'redirect',
|
|
1084
1255
|
true,
|
|
1085
1256
|
);
|
|
@@ -1104,23 +1275,17 @@ export function createHostedUiLauncher(
|
|
|
1104
1275
|
currentOrganizationId?: string,
|
|
1105
1276
|
): Promise<SelectOrganizationResult | null> {
|
|
1106
1277
|
// The list is read on the dashboard origin and never crosses the
|
|
1107
|
-
// bridge; only the chosen organization comes back.
|
|
1108
|
-
// the same reason the install dialog needs it: one sign-in client
|
|
1109
|
-
// serves every app, so the JWT cannot say which app is asking.
|
|
1278
|
+
// bridge; only the chosen organization comes back.
|
|
1110
1279
|
const params: Record<string, unknown> = {};
|
|
1111
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1112
1280
|
if (currentOrganizationId !== undefined) {
|
|
1113
1281
|
params['organizationId'] = currentOrganizationId;
|
|
1114
1282
|
}
|
|
1115
1283
|
return launch<SelectOrganizationResult>('select-organization', params, 'reject');
|
|
1116
1284
|
},
|
|
1117
1285
|
async installApp(organizationId?: string): Promise<InstallAppResult | null> {
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
// user-pool client, so the JWT can't disambiguate). On confirm the
|
|
1121
|
-
// embed page POSTs the install server-side and forwards the response.
|
|
1286
|
+
// The embed page renders the app the init names, with its scopes. On
|
|
1287
|
+
// confirm it POSTs the install server-side and forwards the response.
|
|
1122
1288
|
const params: Record<string, unknown> = {};
|
|
1123
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1124
1289
|
if (organizationId !== undefined) params['organizationId'] = organizationId;
|
|
1125
1290
|
return launch<InstallAppResult>('install-app', params, 'reject');
|
|
1126
1291
|
},
|
|
@@ -1131,11 +1296,7 @@ export function createHostedUiLauncher(
|
|
|
1131
1296
|
organizationId: string,
|
|
1132
1297
|
host: string,
|
|
1133
1298
|
): Promise<ServiceCredentialResult | null> {
|
|
1134
|
-
// `appId` rides the bridge for the same reason the install dialog needs
|
|
1135
|
-
// it: one sign-in client serves every app, so the JWT cannot say which
|
|
1136
|
-
// app is asking, and this screen grants that app in particular.
|
|
1137
1299
|
const params: Record<string, unknown> = { organizationId, host };
|
|
1138
|
-
if (opts.callingAppId !== undefined) params['appId'] = opts.callingAppId;
|
|
1139
1300
|
return launch<ServiceCredentialResult>('service-credential', params, 'reject');
|
|
1140
1301
|
},
|
|
1141
1302
|
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// The importer contract, shared by every place an importer runs.
|
|
2
|
+
//
|
|
3
|
+
// An importer is a leaf: given one file, it returns content as JSON covering
|
|
4
|
+
// one or more named streams of one document, plus an optional report. It
|
|
5
|
+
// holds no Document and calls nothing on Brass. Whatever runs it (the SDK in a
|
|
6
|
+
// browser tab, the platform's import worker) reads the file, encodes what it returns
|
|
7
|
+
// with `encodeImportStreams`, and hands the encoded streams to the platform,
|
|
8
|
+
// which seals them as the document's first content. Nothing here or in a
|
|
9
|
+
// runner knows which importer it is running.
|
|
10
|
+
//
|
|
11
|
+
// Loro-free, so the platform's worker and the published SDK share it.
|
|
12
|
+
|
|
13
|
+
import type { ImportReport } from './import-report.js';
|
|
14
|
+
import { sealedRootKindOf, type SealedStoreBuiltBy } from './sealed-store.js';
|
|
15
|
+
|
|
16
|
+
/** The file an importer is given. */
|
|
17
|
+
export interface ImportFile {
|
|
18
|
+
/** The file's own name, extension included. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** The file's media type, as it was uploaded. */
|
|
21
|
+
content_type: string;
|
|
22
|
+
/** The file's size in bytes. */
|
|
23
|
+
byte_size: number;
|
|
24
|
+
/** @internal A short-lived URL the bytes can be fetched from, where the
|
|
25
|
+
* runner has one. */
|
|
26
|
+
url?: string;
|
|
27
|
+
/** The file's bytes. */
|
|
28
|
+
bytes(): Promise<Uint8Array>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One stream's content as JSON: one entry per top-level collection, the same
|
|
33
|
+
* keys `doc.map`, `doc.list`, `doc.orderedMap` and `doc.text` read it back
|
|
34
|
+
* through. An object is a map (its entries are the records), an array is a
|
|
35
|
+
* list, and a string is a text. An ordered map is its two collections: the
|
|
36
|
+
* map of records and the list of their keys.
|
|
37
|
+
*/
|
|
38
|
+
export type ImportContent = Record<string, Record<string, unknown> | readonly unknown[] | string>;
|
|
39
|
+
|
|
40
|
+
/** What an importer returns: the content of every stream it produces, keyed
|
|
41
|
+
* by stream name, and what it skipped or assumed. The document's own stream
|
|
42
|
+
* is `default`; any other name is a stream an app opens with
|
|
43
|
+
* `doc.stream(name)`. */
|
|
44
|
+
export interface ImportResult {
|
|
45
|
+
streams: Record<string, ImportContent>;
|
|
46
|
+
/** What the import skipped or assumed. Omit it for a file read cleanly. */
|
|
47
|
+
report?: ImportReport;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type DocumentImporter = (
|
|
51
|
+
file: ImportFile,
|
|
52
|
+
) => ImportResult | Promise<ImportResult>;
|
|
53
|
+
|
|
54
|
+
/** The document's own stream. */
|
|
55
|
+
export const DEFAULT_IMPORT_STREAM = 'default';
|
|
56
|
+
|
|
57
|
+
// A stream name is a manifest name token (`NAME_TOKEN_PATTERN` in the
|
|
58
|
+
// platform's contracts): the platform refuses anything else.
|
|
59
|
+
const STREAM_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
60
|
+
|
|
61
|
+
/** A result an importer returned that no runner can store, naming what is
|
|
62
|
+
* wrong with it. */
|
|
63
|
+
export class ImportResultError extends Error {}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The importer's return value, validated, or null when it returned no
|
|
67
|
+
* content. Throws `ImportResultError` naming the stream or collection it
|
|
68
|
+
* could not read.
|
|
69
|
+
*/
|
|
70
|
+
export function readImportResult(value: unknown): ImportResult | null {
|
|
71
|
+
if (value === undefined || value === null) return null;
|
|
72
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
73
|
+
throw new ImportResultError('The importer must return { streams }.');
|
|
74
|
+
}
|
|
75
|
+
const { streams, report } = value as { streams?: unknown; report?: unknown };
|
|
76
|
+
if (streams === undefined || streams === null) return null;
|
|
77
|
+
if (typeof streams !== 'object' || Array.isArray(streams)) {
|
|
78
|
+
throw new ImportResultError(
|
|
79
|
+
'The importer returned streams that are not an object keyed by stream name.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
for (const [name, content] of Object.entries(streams)) {
|
|
83
|
+
if (!STREAM_NAME.test(name)) {
|
|
84
|
+
throw new ImportResultError(
|
|
85
|
+
`The importer returned a stream named "${name}". A stream name is ` +
|
|
86
|
+
'lowercase letters, digits, ".", "-" and "_".',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (content === null || typeof content !== 'object' || Array.isArray(content)) {
|
|
90
|
+
throw new ImportResultError(
|
|
91
|
+
`The importer returned stream "${name}" as something other than an ` +
|
|
92
|
+
'object of collections.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
for (const [key, root] of Object.entries(content as Record<string, unknown>)) {
|
|
96
|
+
if (sealedRootKindOf(root) === null) {
|
|
97
|
+
throw new ImportResultError(
|
|
98
|
+
`The importer returned "${key}" in stream "${name}" as neither an ` +
|
|
99
|
+
'object, an array, nor a string.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
streams: streams as Record<string, ImportContent>,
|
|
106
|
+
...(report !== undefined && report !== null
|
|
107
|
+
? { report: report as ImportReport }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** How many entries a stream's content holds, across its collections. */
|
|
113
|
+
export function importContentEntries(content: ImportContent): number {
|
|
114
|
+
let count = 0;
|
|
115
|
+
for (const root of Object.values(content)) {
|
|
116
|
+
if (typeof root === 'string') count += root === '' ? 0 : 1;
|
|
117
|
+
else if (Array.isArray(root)) count += root.length;
|
|
118
|
+
else count += Object.keys(root).length;
|
|
119
|
+
}
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One stream, encoded in the form its producer uploads
|
|
124
|
+
* (`encodeSealedUpload`). */
|
|
125
|
+
export interface EncodedImportStream {
|
|
126
|
+
name: string;
|
|
127
|
+
bytes: Uint8Array;
|
|
128
|
+
/** Entries the stream holds, counted before the encode consumed them. */
|
|
129
|
+
entries: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Encode every stream a result holds, the default stream first. A stream
|
|
134
|
+
* holding no entries is left out, and a result where every stream is empty
|
|
135
|
+
* encodes to nothing, which a runner reports rather than seals: the import
|
|
136
|
+
* stamp is permanent, and a document stamped with nothing in it has no path
|
|
137
|
+
* back to its file.
|
|
138
|
+
*
|
|
139
|
+
* CONSUMES the result's map values (see `sealedTablesOfImport`).
|
|
140
|
+
*/
|
|
141
|
+
export async function encodeImportStreams(
|
|
142
|
+
result: ImportResult,
|
|
143
|
+
builtBy?: SealedStoreBuiltBy,
|
|
144
|
+
): Promise<EncodedImportStream[]> {
|
|
145
|
+
const { encodeSealedUpload } = await import('./sealed-encode.js');
|
|
146
|
+
const names = Object.keys(result.streams).sort((a, b) =>
|
|
147
|
+
a === DEFAULT_IMPORT_STREAM ? -1 : b === DEFAULT_IMPORT_STREAM ? 1 : 0,
|
|
148
|
+
);
|
|
149
|
+
const out: EncodedImportStream[] = [];
|
|
150
|
+
for (const name of names) {
|
|
151
|
+
const content = result.streams[name]!;
|
|
152
|
+
const entries = importContentEntries(content);
|
|
153
|
+
if (entries === 0) continue;
|
|
154
|
+
out.push({ name, bytes: await encodeSealedUpload(content, builtBy), entries });
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- The staging bundle -------------------------------------------------
|
|
160
|
+
//
|
|
161
|
+
// A runner outside the platform stages every stream of one import as ONE
|
|
162
|
+
// object, so the seal reads one upload and the streams land together or not
|
|
163
|
+
// at all. `BRSB`, a version byte, the stream count, then per stream a u16
|
|
164
|
+
// name length, the name, a u32 byte length, and the bytes.
|
|
165
|
+
|
|
166
|
+
const BUNDLE_MAGIC = 0x42_52_53_42;
|
|
167
|
+
const BUNDLE_VERSION = 1;
|
|
168
|
+
|
|
169
|
+
export function encodeStreamBundle(
|
|
170
|
+
streams: readonly { name: string; bytes: Uint8Array }[],
|
|
171
|
+
): Uint8Array {
|
|
172
|
+
const encoder = new TextEncoder();
|
|
173
|
+
const names = streams.map((s) => encoder.encode(s.name));
|
|
174
|
+
let total = 9;
|
|
175
|
+
for (let i = 0; i < streams.length; i += 1) {
|
|
176
|
+
total += 2 + names[i]!.byteLength + 4 + streams[i]!.bytes.byteLength;
|
|
177
|
+
}
|
|
178
|
+
const out = new Uint8Array(total);
|
|
179
|
+
const view = new DataView(out.buffer);
|
|
180
|
+
view.setUint32(0, BUNDLE_MAGIC);
|
|
181
|
+
view.setUint8(4, BUNDLE_VERSION);
|
|
182
|
+
view.setUint32(5, streams.length);
|
|
183
|
+
let at = 9;
|
|
184
|
+
for (let i = 0; i < streams.length; i += 1) {
|
|
185
|
+
const name = names[i]!;
|
|
186
|
+
view.setUint16(at, name.byteLength);
|
|
187
|
+
at += 2;
|
|
188
|
+
out.set(name, at);
|
|
189
|
+
at += name.byteLength;
|
|
190
|
+
const bytes = streams[i]!.bytes;
|
|
191
|
+
view.setUint32(at, bytes.byteLength);
|
|
192
|
+
at += 4;
|
|
193
|
+
out.set(bytes, at);
|
|
194
|
+
at += bytes.byteLength;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The streams a bundle carries, or null for bytes that are not a bundle this
|
|
200
|
+
* reader can read. */
|
|
201
|
+
export function decodeStreamBundle(
|
|
202
|
+
bytes: Uint8Array,
|
|
203
|
+
): { name: string; bytes: Uint8Array }[] | null {
|
|
204
|
+
if (bytes.byteLength < 9) return null;
|
|
205
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
206
|
+
if (view.getUint32(0) !== BUNDLE_MAGIC || view.getUint8(4) !== BUNDLE_VERSION) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const count = view.getUint32(5);
|
|
210
|
+
const decoder = new TextDecoder();
|
|
211
|
+
const out: { name: string; bytes: Uint8Array }[] = [];
|
|
212
|
+
let at = 9;
|
|
213
|
+
for (let i = 0; i < count; i += 1) {
|
|
214
|
+
if (at + 2 > bytes.byteLength) return null;
|
|
215
|
+
const nameLength = view.getUint16(at);
|
|
216
|
+
at += 2;
|
|
217
|
+
if (at + nameLength + 4 > bytes.byteLength) return null;
|
|
218
|
+
const name = decoder.decode(bytes.subarray(at, at + nameLength));
|
|
219
|
+
at += nameLength;
|
|
220
|
+
const length = view.getUint32(at);
|
|
221
|
+
at += 4;
|
|
222
|
+
if (at + length > bytes.byteLength) return null;
|
|
223
|
+
out.push({ name, bytes: bytes.subarray(at, at + length) });
|
|
224
|
+
at += length;
|
|
225
|
+
}
|
|
226
|
+
return at === bytes.byteLength ? out : null;
|
|
227
|
+
}
|