mindoodb-app-sdk 0.0.40 → 0.0.42
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/README.md +94 -3
- package/TESTING.md +1 -1
- package/dist/appBundleManifest-B0xfpaXG.d.ts +62 -0
- package/dist/chunk-6433L6TE.js +1303 -0
- package/dist/chunk-LT4FK6LU.js +146 -0
- package/dist/index.d.ts +91 -3
- package/dist/index.js +280 -1170
- package/dist/testing/index.d.ts +11 -1
- package/dist/testing/index.js +112 -13
- package/dist/{types-DlZpHkfb.d.ts → types-DxnCg5IW.d.ts} +106 -2
- package/dist/vite/index.d.ts +46 -0
- package/dist/vite/index.js +291 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -412,6 +412,8 @@ await db.documents.undelete(docId);
|
|
|
412
412
|
|
|
413
413
|
Every `MindooDBAppDocument` snapshot also exposes a `heads?: string[]` field. Those heads identify the document's current Automerge version and are the value you pass to `baseHeads` when issuing the next text patch. After Haven applies and merges your patch, the returned document carries the new `heads` you should use for the following edit cycle.
|
|
414
414
|
|
|
415
|
+
A snapshot also carries `decryptionKeyId?: string` — the named shared key its payload is encrypted with (`"default"` unless the document was created with another key). That is the key someone else needs access to before they can open the document, so show it wherever an app offers sharing. It is absent for person-encrypted documents (created with `recipients`), which have no shareable named key: those are shared by editing the recipient list. It is also absent on Haven versions older than this field.
|
|
416
|
+
|
|
415
417
|
`documents.list()` is backed by Haven's internal changefeed, not by a positional offset. The query object supports:
|
|
416
418
|
|
|
417
419
|
| Field | Meaning |
|
|
@@ -455,7 +457,8 @@ const history = await db.documents.listHistory(docId);
|
|
|
455
457
|
// isDeleted,
|
|
456
458
|
// isCurrent,
|
|
457
459
|
// summary?,
|
|
458
|
-
// dependencyIds
|
|
460
|
+
// dependencyIds?,
|
|
461
|
+
// decryptionKeyId?
|
|
459
462
|
// }
|
|
460
463
|
|
|
461
464
|
// Load the document state at an exact DAG revision
|
|
@@ -504,6 +507,8 @@ const merged = await db.documents.getAtHeads(docId, entry.dependencyIds ?? []);
|
|
|
504
507
|
|
|
505
508
|
An id in `dependencyIds` may name a revision the timeline does not list, because a change that left the document state untouched produces no timeline row. Draw only the edges whose endpoints you have; `getAtHeads` still resolves the full list.
|
|
506
509
|
|
|
510
|
+
`decryptionKeyId` is per revision, so a document whose key changed over its life shows that in the timeline — useful when deciding which key to grant before handing an old version to someone. It follows the same rule as on `documents.get`: absent for person-encrypted revisions.
|
|
511
|
+
|
|
507
512
|
### Granular JSON edits
|
|
508
513
|
|
|
509
514
|
When you need to edit nested objects or lists inside a document — not just top-level fields — describe the change as a **granular JSON patch** rather than rewriting the whole document. This is the model behind structured collaborative editing in apps like [`mindoodb-app-teamgrid`](https://github.com/klehmann/mindoodb-app-teamgrid), where two users can insert different rows into the same worksheet on different devices and the merge keeps both sides cleanly.
|
|
@@ -1467,7 +1472,70 @@ wrangler deploy
|
|
|
1467
1472
|
|
|
1468
1473
|
Other static hosting options work just as well: Netlify, Vercel, any web server serving your `dist/` folder.
|
|
1469
1474
|
|
|
1470
|
-
|
|
1475
|
+
### Haven-hosted bundles
|
|
1476
|
+
|
|
1477
|
+
Instead of Haven loading your app from your server on every launch, you can hand Haven the build itself. Haven stores it in its own cache and serves it from a service worker on an opaque origin, so the app loads offline and runs in a stricter sandbox than an external URL gets.
|
|
1478
|
+
|
|
1479
|
+
You can upload a build directory by hand, but the useful form is to publish a **bundle manifest** next to your app and let Haven fetch and update it. Add the plugin to your Vite config:
|
|
1480
|
+
|
|
1481
|
+
```ts
|
|
1482
|
+
import { havenBundle } from "mindoodb-app-sdk/vite";
|
|
1483
|
+
|
|
1484
|
+
export default defineConfig({
|
|
1485
|
+
// Haven serves the bundle under a path it picks, so assets must be relative.
|
|
1486
|
+
base: "./",
|
|
1487
|
+
plugins: [havenBundle()],
|
|
1488
|
+
});
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
After `vite build`, `dist/` additionally contains `haven-bundle.json` (app id, version, a SHA-256 for every file, and one for the bundle as a whole) and `haven-bundle.zip` (all of it in one archive). The app's own service worker files are left out — Haven serves the bundle and a second service worker would only fight it for the scope. The user pastes the URL of `haven-bundle.json` into Haven's application settings; Haven downloads the archive, rejects it unless every hash matches the manifest, and installs it.
|
|
1492
|
+
|
|
1493
|
+
Haven then owns updates. It re-reads the manifest before launches and on a slow background timer, and installs a new version when the `contentHash` changes — which means an app in hosted mode should **not** run its own update logic:
|
|
1494
|
+
|
|
1495
|
+
```ts
|
|
1496
|
+
import { isHostedBundleRuntime } from "mindoodb-app-sdk";
|
|
1497
|
+
|
|
1498
|
+
if (!isHostedBundleRuntime()) {
|
|
1499
|
+
registerServiceWorker();
|
|
1500
|
+
}
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
Serving the manifest and archive to Haven needs CORS, since Haven is on a different origin than your app:
|
|
1504
|
+
|
|
1505
|
+
```
|
|
1506
|
+
/haven-bundle.json
|
|
1507
|
+
Access-Control-Allow-Origin: *
|
|
1508
|
+
Cache-Control: no-cache
|
|
1509
|
+
|
|
1510
|
+
/haven-bundle.zip
|
|
1511
|
+
Access-Control-Allow-Origin: *
|
|
1512
|
+
Cache-Control: public, max-age=300
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
The manifest must not be cached — it is the signal that a new version exists. The archive can be, because the manifest pins it by hash.
|
|
1516
|
+
|
|
1517
|
+
#### Storage in hosted mode
|
|
1518
|
+
|
|
1519
|
+
An opaque origin has no storage: touching `localStorage` throws a `SecurityError`. The host keeps a small amount of key/value state for the app instead, reachable at `session.storage`:
|
|
1520
|
+
|
|
1521
|
+
```ts
|
|
1522
|
+
await session.storage.set("my-app.theme", "dark");
|
|
1523
|
+
const theme = await session.storage.get("my-app.theme");
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
That is asynchronous, and most existing app code reads preferences synchronously during render. For those, install a shim before the app boots. It replaces the global `localStorage` with one hydrated from the host, serves reads from memory, and writes keys under the given prefixes back to the host in the background:
|
|
1527
|
+
|
|
1528
|
+
```ts
|
|
1529
|
+
// entry point, before importing the app
|
|
1530
|
+
await installHavenStorageShim({ persistPrefixes: ["my-app-"] });
|
|
1531
|
+
await import("./main");
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
Outside hosted mode this does nothing and the real `localStorage` stays in place. Keys outside `persistPrefixes` still work but live only for the session — a deliberate choice, since the budget is meant for preferences.
|
|
1535
|
+
|
|
1536
|
+
Two things are worth knowing when you use it. Writes are coalesced, so a value written on every `pointermove` costs one message rather than sixty per second; still, prefer committing such a value once when the interaction ends. And the budget is finite (256 KB per installed app, 64 KB per value), so `setItem` can throw `QuotaExceededError` just as it can in a browser. Anything larger than a preference belongs in a database.
|
|
1537
|
+
|
|
1538
|
+
Haven asks the app to flush pending writes before it tears the iframe down, so a preference set immediately before closing is not lost. The shim answers that on its own.
|
|
1471
1539
|
|
|
1472
1540
|
## Local development workflow
|
|
1473
1541
|
|
|
@@ -1505,10 +1573,25 @@ Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
|
|
|
1505
1573
|
| `closeSealedChannel(channelId)` | `Promise<void>` |
|
|
1506
1574
|
| `menus.show(input)` | `Promise<MindooDBAppShowMenuResult>` |
|
|
1507
1575
|
| `menus.hide()` | `Promise<void>` |
|
|
1576
|
+
| `storage` | `MindooDBAppStorageApi` |
|
|
1508
1577
|
| `onThemeChange(listener)` | `() => void` (unsubscribe) |
|
|
1509
1578
|
| `onViewportChange(listener)` | `() => void` (unsubscribe) |
|
|
1579
|
+
| `onBeforeClose(listener)` | `() => void` (unsubscribe) |
|
|
1510
1580
|
| `disconnect()` | `Promise<void>` |
|
|
1511
1581
|
|
|
1582
|
+
### MindooDBAppStorageApi
|
|
1583
|
+
|
|
1584
|
+
Host-kept key/value state, the only durable storage a hosted app has. See [Storage in hosted mode](#storage-in-hosted-mode).
|
|
1585
|
+
|
|
1586
|
+
| Method | Returns |
|
|
1587
|
+
| --------------------- | ----------------------------------- |
|
|
1588
|
+
| `snapshot(options?)` | `Promise<Record<string, string>>` |
|
|
1589
|
+
| `get(key)` | `Promise<string \| null>` |
|
|
1590
|
+
| `set(key, value)` | `Promise<void>` |
|
|
1591
|
+
| `remove(key)` | `Promise<void>` |
|
|
1592
|
+
| `keys(options?)` | `Promise<string[]>` |
|
|
1593
|
+
| `clear(options?)` | `Promise<void>` |
|
|
1594
|
+
|
|
1512
1595
|
### MindooDBAppDatabase
|
|
1513
1596
|
|
|
1514
1597
|
| Property / Method | Returns |
|
|
@@ -1549,7 +1632,7 @@ Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
|
|
|
1549
1632
|
|
|
1550
1633
|
`list(query?)` accepts the changefeed query options documented above. The `cursor` value is an opaque checkpoint string managed by Haven and should be stored and passed back unchanged.
|
|
1551
1634
|
|
|
1552
|
-
`listInaccessible(query?)` returns documents that exist locally but the current user cannot decrypt (not a recipient, or missing the document key). Those ids never appear in `list` / `get`. Each row is unsigned store metadata only (`id`, `createdAt`, `decryptionKeyId`, optional `authorLabel`) — the app does not get CAS access. Use it to distinguish an empty database from one sealed to someone else.
|
|
1635
|
+
`listInaccessible(query?)` returns documents that exist locally but the current user cannot decrypt (not a recipient, or missing the document key). Those ids never appear in `list` / `get`. Each row is unsigned store metadata only (`id`, `createdAt`, `decryptionKeyId`, optional `authorLabel`) — the app does not get CAS access. Use it to distinguish an empty database from one sealed to someone else. For documents the user *can* open, the same key id is on `documents.get` and on each `listHistory` entry; `documents.list` rows do not carry it.
|
|
1553
1636
|
|
|
1554
1637
|
For `create(input)`, use `MindooDBAppCreateDocumentInput` with shape `{ set: Record<string, unknown>; decryptionKeyId?: string; recipients?: string[]; recipientOptions?: { includeSelf?: boolean }; id?: string }`. Omit both `decryptionKeyId` and `recipients` to use the database default shared key (`getDefaultCreateKeyId()` / `listCreateKeys()`). Pass `recipients` to encrypt the document for specific directory users (the launching user is included unless `recipientOptions.includeSelf` is `false`). Use `includeSelf: false` to write initial `set` values and hand the document to someone else — empty `recipients` with `includeSelf: false` is rejected. Changing that list later is `addRecipients` / `removeRecipients` / `setRecipients` — do not write `_encryptFor` via `update`. The optional `id` lets the app pick the document id instead of letting MindooDB generate an ObjectId; when provided it must match `/^[a-z][a-z0-9_]*$/` (lowercase only, since ids become on-disk filenames) and existing live documents with that id are returned as-is (idempotent create-if-missing). If an existing custom-id document is deleted, `create({ id })` undeletes it and preserves the previous document body. Documents created with the same custom id on different replicas converge correctly when synced.
|
|
1555
1638
|
|
|
@@ -1637,6 +1720,14 @@ interface MindooDBAppUpdateDocumentInput {
|
|
|
1637
1720
|
| `createViewLanguage<T>()` | Create a typed expression builder for view definitions |
|
|
1638
1721
|
| `abbreviateCanonicalName(value)` | Convert a canonical Notes-style name like `cn=Jane/ou=Dev/o=Mindoo` to `Jane/Dev/Mindoo` |
|
|
1639
1722
|
| `expandAbbreviatedName(value)` | Convert an abbreviated Notes-style name like `Jane/Dev/Mindoo` to `cn=Jane/ou=Dev/o=Mindoo` |
|
|
1723
|
+
| `isHostedBundleRuntime(search?)` | Whether Haven is serving this app as a hosted bundle rather than from its own URL |
|
|
1724
|
+
| `installHavenStorageShim(options?)` | Replace `localStorage` with a host-backed shim in hosted mode; a no-op everywhere else |
|
|
1725
|
+
|
|
1726
|
+
The Vite plugin lives behind its own entrypoint:
|
|
1727
|
+
|
|
1728
|
+
| Function | Import from | Description |
|
|
1729
|
+
| ----------------------- | ------------------------- | -------------------------------------------------------- |
|
|
1730
|
+
| `havenBundle(options?)` | `mindoodb-app-sdk/vite` | Emit `haven-bundle.json` and `haven-bundle.zip` on build |
|
|
1640
1731
|
|
|
1641
1732
|
## Permissions and mappings
|
|
1642
1733
|
|
package/TESTING.md
CHANGED
|
@@ -102,7 +102,7 @@ When you pass `databases`, the mock session also exposes them through `session.g
|
|
|
102
102
|
Each database entry can provide:
|
|
103
103
|
|
|
104
104
|
- `info`
|
|
105
|
-
- `documents` — seed documents into the mock store (used by `list`/`query` and by evaluating VirtualViews)
|
|
105
|
+
- `documents` — seed documents into the mock store (used by `list`/`query` and by evaluating VirtualViews). A seed's `decryptionKeyId` is reported back by `documents.get`, like a real host; documents created through the mock report `"default"` unless `create` named a key, and person-encrypted documents (`recipients`) report none.
|
|
106
106
|
- `methods.documents`
|
|
107
107
|
- `methods.views` for session-level `createView()` and `openView()` calls (overrides the default evaluating navigator)
|
|
108
108
|
- `methods.attachments`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared description of a built application bundle that a MindooDB host can install
|
|
3
|
+
* and update from a URL.
|
|
4
|
+
*
|
|
5
|
+
* The build side (`mindoodb-app-sdk/vite`) writes the manifest, the host side reads and
|
|
6
|
+
* validates it. Both import this module so the format has exactly one definition.
|
|
7
|
+
*/
|
|
8
|
+
declare const MINDOODB_APP_BUNDLE_MANIFEST_FORMAT = "mindoodb.app.bundle";
|
|
9
|
+
declare const MINDOODB_APP_BUNDLE_MANIFEST_VERSION = 1;
|
|
10
|
+
declare const MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME = "haven-bundle.json";
|
|
11
|
+
declare const MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME = "haven-bundle.zip";
|
|
12
|
+
/** A `sha256-<64 lowercase hex chars>` digest string. */
|
|
13
|
+
type MindooDBAppBundleHash = string;
|
|
14
|
+
interface MindooDBAppBundleFileEntry {
|
|
15
|
+
/** Bundle-relative POSIX path, e.g. `assets/index-a1b2c3.js`. */
|
|
16
|
+
path: string;
|
|
17
|
+
hash: MindooDBAppBundleHash;
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
interface MindooDBAppBundleArchive {
|
|
21
|
+
/** Path of the archive relative to the manifest URL. */
|
|
22
|
+
path: string;
|
|
23
|
+
hash: MindooDBAppBundleHash;
|
|
24
|
+
size: number;
|
|
25
|
+
}
|
|
26
|
+
interface MindooDBAppBundleManifest {
|
|
27
|
+
format: typeof MINDOODB_APP_BUNDLE_MANIFEST_FORMAT;
|
|
28
|
+
formatVersion: number;
|
|
29
|
+
/** Stable identifier of the application, used to detect a swapped bundle source. */
|
|
30
|
+
appId: string;
|
|
31
|
+
/** Human-readable version. Informational only — updates key off `contentHash`. */
|
|
32
|
+
version: string;
|
|
33
|
+
/** Entry HTML file, always present in `files`. */
|
|
34
|
+
entry: string;
|
|
35
|
+
archive: MindooDBAppBundleArchive;
|
|
36
|
+
/** Identity of the bundle: a digest over the sorted file list. */
|
|
37
|
+
contentHash: MindooDBAppBundleHash;
|
|
38
|
+
generatedAt: string;
|
|
39
|
+
files: MindooDBAppBundleFileEntry[];
|
|
40
|
+
}
|
|
41
|
+
declare function isMindooDBAppBundleHash(value: unknown): value is MindooDBAppBundleHash;
|
|
42
|
+
/**
|
|
43
|
+
* Mirrors the host-side path rules: POSIX separators, no leading slash, no `.`/`..`
|
|
44
|
+
* segments and no empty segments. Returns `null` for paths that must be rejected.
|
|
45
|
+
*/
|
|
46
|
+
declare function normalizeMindooDBAppBundlePath(rawPath: unknown): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Canonical byte input for `contentHash`. Both the build and the host derive the digest
|
|
49
|
+
* from this exact string so a mismatch always means the content really differs.
|
|
50
|
+
*/
|
|
51
|
+
declare function buildMindooDBAppBundleContentHashInput(files: readonly Pick<MindooDBAppBundleFileEntry, "path" | "hash">[]): string;
|
|
52
|
+
interface MindooDBAppBundleManifestValidation {
|
|
53
|
+
manifest: MindooDBAppBundleManifest | null;
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Structural validation only — it does not verify any digest. Callers still have to
|
|
58
|
+
* compare `contentHash` and the per-file hashes against the bytes they downloaded.
|
|
59
|
+
*/
|
|
60
|
+
declare function validateMindooDBAppBundleManifest(raw: unknown): MindooDBAppBundleManifestValidation;
|
|
61
|
+
|
|
62
|
+
export { MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME as M, MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME as a, MINDOODB_APP_BUNDLE_MANIFEST_FORMAT as b, MINDOODB_APP_BUNDLE_MANIFEST_VERSION as c, type MindooDBAppBundleArchive as d, type MindooDBAppBundleFileEntry as e, type MindooDBAppBundleHash as f, type MindooDBAppBundleManifest as g, type MindooDBAppBundleManifestValidation as h, buildMindooDBAppBundleContentHashInput as i, isMindooDBAppBundleHash as j, normalizeMindooDBAppBundlePath as n, validateMindooDBAppBundleManifest as v };
|