mindoodb-app-sdk 0.0.53 → 0.0.55

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/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  MINDOODB_APP_MAX_DRAG_TYPES,
9
9
  MINDOODB_APP_VALUE_TAG,
10
10
  MINIMAL_DRAG_PREVIEW_PNG,
11
+ MindooDBAppAgentToolError,
11
12
  MindooDBAppDragInputError,
12
13
  MindooDBAppValue,
13
14
  applyHostedDocumentOverscrollContain,
@@ -23,7 +24,7 @@ import {
23
24
  releaseMindooDBAppBridgeSessions,
24
25
  sanitizeHostShortcutBindings,
25
26
  snapshotElementToPng
26
- } from "./chunk-6NTVTL6G.js";
27
+ } from "./chunk-S73KTYZF.js";
27
28
  import {
28
29
  MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
29
30
  MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
@@ -286,6 +287,13 @@ function readListing(value, errors) {
286
287
  errors
287
288
  );
288
289
  if (description) listing.description = description;
290
+ const descriptionMarkdown = readLocalizedText(
291
+ value.descriptionMarkdown,
292
+ "App definition listing.descriptionMarkdown",
293
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
294
+ errors
295
+ );
296
+ if (descriptionMarkdown) listing.descriptionMarkdown = descriptionMarkdown;
289
297
  const icon = readListingAssetPath(value.icon, "App definition listing.icon", errors);
290
298
  if (icon) listing.icon = icon;
291
299
  if (value.screenshots !== void 0 && value.screenshots !== null) {
@@ -575,6 +583,212 @@ function isHostedBundleRuntime(search) {
575
583
  return readMindooDBAppHostingMode(search) === "hosted";
576
584
  }
577
585
 
586
+ // src/listingMarkdown.ts
587
+ var MAX_INLINE_DEPTH = 4;
588
+ var HEADING = /^#{1,6}\s+(.*?)\s*#*\s*$/;
589
+ var BULLET = /^[-*+]\s+(.*)$/;
590
+ var ORDERED = /^\d{1,9}[.)]\s+(.*)$/;
591
+ var ESCAPABLE = new Set("\\`*_[]()#+-.!<>".split(""));
592
+ var BARE_URL_TRAILING_PUNCTUATION = /[.,;:!?)\]'"]+$/;
593
+ function sanitizeListingHref(raw) {
594
+ const cleaned = raw.replace(/[\t\n\r]/g, "").trim();
595
+ if (!cleaned) {
596
+ return null;
597
+ }
598
+ try {
599
+ const url = new URL(cleaned);
600
+ if (url.protocol === "https:") {
601
+ return url.toString();
602
+ }
603
+ if (url.protocol === "mailto:" && url.pathname) {
604
+ return url.toString();
605
+ }
606
+ } catch {
607
+ }
608
+ return null;
609
+ }
610
+ function isWordCharacter(char) {
611
+ return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
612
+ }
613
+ function pushText(nodes, text) {
614
+ if (!text) {
615
+ return;
616
+ }
617
+ const last = nodes[nodes.length - 1];
618
+ if (last?.type === "text") {
619
+ last.text += text;
620
+ } else {
621
+ nodes.push({ type: "text", text });
622
+ }
623
+ }
624
+ function findClosingDelimiter(text, from, delimiter) {
625
+ let index = text.indexOf(delimiter, from);
626
+ while (index !== -1) {
627
+ const before = text[index - 1];
628
+ const after = text[index + delimiter.length];
629
+ const closesAfterContent = index > from && before !== " " && before !== "\\";
630
+ const underscoreBoundary = delimiter[0] !== "_" || !isWordCharacter(after);
631
+ if (closesAfterContent && underscoreBoundary) {
632
+ return index;
633
+ }
634
+ index = text.indexOf(delimiter, index + 1);
635
+ }
636
+ return -1;
637
+ }
638
+ function findLinkTargetEnd(text, from) {
639
+ let open = 0;
640
+ for (let index = from; index < text.length; index += 1) {
641
+ const char = text[index];
642
+ if (char === "(") {
643
+ open += 1;
644
+ } else if (char === ")") {
645
+ if (open === 0) return index;
646
+ open -= 1;
647
+ } else if (char === " ") {
648
+ return -1;
649
+ }
650
+ }
651
+ return -1;
652
+ }
653
+ function parseInline(text, depth = 0) {
654
+ const nodes = [];
655
+ let index = 0;
656
+ while (index < text.length) {
657
+ const char = text[index];
658
+ if (char === "\\" && ESCAPABLE.has(text[index + 1] ?? "")) {
659
+ pushText(nodes, text[index + 1]);
660
+ index += 2;
661
+ continue;
662
+ }
663
+ if (char === "`") {
664
+ const end = text.indexOf("`", index + 1);
665
+ if (end > index + 1) {
666
+ nodes.push({ type: "code", text: text.slice(index + 1, end) });
667
+ index = end + 1;
668
+ continue;
669
+ }
670
+ }
671
+ if (depth < MAX_INLINE_DEPTH && (char === "*" || char === "_")) {
672
+ const strong = text[index + 1] === char;
673
+ const delimiter = strong ? char + char : char;
674
+ const next = text[index + delimiter.length];
675
+ const opens = next !== void 0 && next !== " " && (char !== "_" || !isWordCharacter(text[index - 1]));
676
+ if (opens) {
677
+ const end = findClosingDelimiter(text, index + delimiter.length, delimiter);
678
+ if (end !== -1) {
679
+ const children = parseInline(text.slice(index + delimiter.length, end), depth + 1);
680
+ nodes.push(strong ? { type: "strong", children } : { type: "em", children });
681
+ index = end + delimiter.length;
682
+ continue;
683
+ }
684
+ }
685
+ }
686
+ if (char === "[" && depth < MAX_INLINE_DEPTH) {
687
+ const labelEnd = text.indexOf("](", index + 1);
688
+ const urlEnd = labelEnd === -1 ? -1 : findLinkTargetEnd(text, labelEnd + 2);
689
+ if (labelEnd > index + 1 && urlEnd !== -1) {
690
+ const label = parseInline(text.slice(index + 1, labelEnd), depth + 1);
691
+ const href = sanitizeListingHref(text.slice(labelEnd + 2, urlEnd));
692
+ if (href) {
693
+ nodes.push({ type: "link", href, children: label });
694
+ } else {
695
+ nodes.push(...label);
696
+ }
697
+ index = urlEnd + 1;
698
+ continue;
699
+ }
700
+ }
701
+ if (char === "h" && text.startsWith("https://", index) && !isWordCharacter(text[index - 1])) {
702
+ let end = index;
703
+ while (end < text.length && !/\s/.test(text[end])) end += 1;
704
+ const candidate = text.slice(index, end).replace(BARE_URL_TRAILING_PUNCTUATION, "");
705
+ const href = sanitizeListingHref(candidate);
706
+ if (href) {
707
+ nodes.push({ type: "link", href, children: [{ type: "text", text: candidate }] });
708
+ index += candidate.length;
709
+ continue;
710
+ }
711
+ }
712
+ pushText(nodes, char);
713
+ index += 1;
714
+ }
715
+ return nodes;
716
+ }
717
+ function parseListingMarkdown(markdown) {
718
+ const blocks = [];
719
+ let paragraph = [];
720
+ let list = null;
721
+ const flushParagraph = () => {
722
+ if (paragraph.length) {
723
+ blocks.push({ type: "paragraph", children: parseInline(paragraph.join(" ")) });
724
+ paragraph = [];
725
+ }
726
+ };
727
+ const flushList = () => {
728
+ if (list) {
729
+ blocks.push({ type: "list", ordered: list.ordered, items: list.items.map((item) => parseInline(item)) });
730
+ list = null;
731
+ }
732
+ };
733
+ for (const rawLine of markdown.split(/\r?\n/)) {
734
+ const line = rawLine.trim();
735
+ if (!line) {
736
+ flushParagraph();
737
+ flushList();
738
+ continue;
739
+ }
740
+ const heading = HEADING.exec(line);
741
+ const bullet = BULLET.exec(line);
742
+ const ordered = bullet ? null : ORDERED.exec(line);
743
+ if (heading) {
744
+ flushParagraph();
745
+ flushList();
746
+ blocks.push({ type: "heading", children: parseInline(heading[1]) });
747
+ } else if (bullet || ordered) {
748
+ flushParagraph();
749
+ const isOrdered = Boolean(ordered);
750
+ if (list && list.ordered !== isOrdered) {
751
+ flushList();
752
+ }
753
+ list ??= { ordered: isOrdered, items: [] };
754
+ list.items.push((bullet ?? ordered)[1]);
755
+ } else if (list) {
756
+ list.items[list.items.length - 1] += ` ${line}`;
757
+ } else {
758
+ paragraph.push(line);
759
+ }
760
+ }
761
+ flushParagraph();
762
+ flushList();
763
+ return blocks;
764
+ }
765
+ function inlineToPlainText(nodes) {
766
+ return nodes.map((node) => {
767
+ if (node.type === "text" || node.type === "code") {
768
+ return node.text;
769
+ }
770
+ const label = inlineToPlainText(node.children);
771
+ if (node.type !== "link") {
772
+ return label;
773
+ }
774
+ const target = node.href.replace(/^mailto:/, "").replace(/^https:\/\//, "").replace(/\/$/, "");
775
+ return label.includes(target) ? label : `${label} (${node.href.replace(/^mailto:/, "")})`;
776
+ }).join("");
777
+ }
778
+ function listingMarkdownToPlainText(markdown) {
779
+ const paragraphs = [];
780
+ for (const block of parseListingMarkdown(markdown)) {
781
+ if (block.type === "list") {
782
+ block.items.forEach((item, index) => {
783
+ paragraphs.push(`${block.ordered ? `${index + 1}.` : "\u2022"} ${inlineToPlainText(item)}`);
784
+ });
785
+ } else {
786
+ paragraphs.push(inlineToPlainText(block.children));
787
+ }
788
+ }
789
+ return paragraphs.filter((paragraph) => paragraph.trim()).join("\n\n");
790
+ }
791
+
578
792
  // src/landing.ts
579
793
  var DEFAULT_HAVEN_URL = "https://haven.mindoodb.com";
580
794
  var APP_ICON_FALLBACKS = ["appicon.png", "appicon.webp", "appicon.svg"];
@@ -638,6 +852,43 @@ function el(tag, className, text) {
638
852
  if (text !== void 0) node.textContent = text;
639
853
  return node;
640
854
  }
855
+ function externalLink(href) {
856
+ const link = el("a");
857
+ link.href = href;
858
+ link.rel = "noopener noreferrer";
859
+ link.target = "_blank";
860
+ return link;
861
+ }
862
+ function appendInline(parent, nodes) {
863
+ for (const node of nodes) {
864
+ if (node.type === "text") {
865
+ parent.append(node.text);
866
+ } else if (node.type === "code") {
867
+ parent.append(el("code", void 0, node.text));
868
+ } else {
869
+ const child = node.type === "link" ? externalLink(node.href) : el(node.type);
870
+ appendInline(child, node.children);
871
+ parent.append(child);
872
+ }
873
+ }
874
+ }
875
+ function renderMarkdownDescription(block, markdown) {
876
+ for (const entry of parseListingMarkdown(markdown)) {
877
+ if (entry.type === "list") {
878
+ const list = el(entry.ordered ? "ol" : "ul");
879
+ for (const item of entry.items) {
880
+ const li = el("li");
881
+ appendInline(li, item);
882
+ list.append(li);
883
+ }
884
+ block.append(list);
885
+ } else {
886
+ const node = el(entry.type === "heading" ? "h2" : "p");
887
+ appendInline(node, entry.children);
888
+ block.append(node);
889
+ }
890
+ }
891
+ }
641
892
  var STYLE_ID = "mdb-landing-style";
642
893
  var STYLE = `
643
894
  .mdb-landing{--bg:#f4f6fb;--card:#fff;--text:#172033;--muted:#5b6478;--line:#e2e6ef;--accent:#1f3a8a;--accent-text:#fff;
@@ -657,7 +908,12 @@ var STYLE = `
657
908
  .mdb-landing__button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
658
909
  .mdb-landing__hint{margin:0;max-width:420px;color:var(--muted);font-size:14px}
659
910
  .mdb-landing__description{margin:32px 0 0}
660
- .mdb-landing__description p{margin:0 0 12px}
911
+ .mdb-landing__description p,.mdb-landing__description ul,.mdb-landing__description ol{margin:0 0 12px}
912
+ .mdb-landing__description ul,.mdb-landing__description ol{padding-left:22px}
913
+ .mdb-landing__description li+li{margin-top:4px}
914
+ .mdb-landing__description h2{margin:20px 0 8px;font-size:17px}
915
+ .mdb-landing__description code{font:.92em ui-monospace,SFMono-Regular,Menlo,monospace;padding:1px 5px;border-radius:5px;background:var(--line)}
916
+ .mdb-landing__description a,.mdb-landing__meta a{color:var(--accent)}
661
917
  .mdb-landing__shots-title{margin:32px 0 12px;font-size:15px;color:var(--muted);font-weight:600}
662
918
  .mdb-landing__shots{display:grid;grid-auto-flow:column;grid-auto-columns:min(78%,420px);gap:16px;overflow-x:auto;padding-bottom:8px;scroll-snap-type:x mandatory}
663
919
  .mdb-landing__shot{margin:0;scroll-snap-align:start}
@@ -706,11 +962,14 @@ async function renderHavenAppLandingPage(options = {}) {
706
962
  }
707
963
  const titles = el("div");
708
964
  titles.append(el("p", "mdb-landing__eyebrow", strings.eyebrow), el("h1", "mdb-landing__title", label));
709
- const meta = [format(strings.from, { origin })];
965
+ const meta = el("p", "mdb-landing__meta");
710
966
  if (listing?.publisher) {
711
- meta.unshift(format(strings.by, { publisher: listing.publisher.name }));
967
+ const [before, after = ""] = strings.by.split("{publisher}");
968
+ const name = listing.publisher.url ? Object.assign(externalLink(listing.publisher.url), { textContent: listing.publisher.name }) : listing.publisher.name;
969
+ meta.append(before ?? "", name, after, " \xB7 ");
712
970
  }
713
- titles.append(el("p", "mdb-landing__meta", meta.join(" \xB7 ")));
971
+ meta.append(format(strings.from, { origin }));
972
+ titles.append(meta);
714
973
  head.append(titles);
715
974
  card.append(head);
716
975
  const summary = resolveMindooDBAppLocalizedText(listing?.summary, locale) || definition?.description || strings.fallbackSummary;
@@ -722,11 +981,16 @@ async function renderHavenAppLandingPage(options = {}) {
722
981
  button.dataset.testid = "haven-app-landing-install";
723
982
  cta.append(button, el("p", "mdb-landing__hint", format(strings.ctaHint, { label })));
724
983
  card.append(cta);
984
+ const descriptionMarkdown = resolveMindooDBAppLocalizedText(listing?.descriptionMarkdown, locale);
725
985
  const description = resolveMindooDBAppLocalizedText(listing?.description, locale);
726
- if (description) {
986
+ if (descriptionMarkdown || description) {
727
987
  const block = el("section", "mdb-landing__description");
728
- for (const paragraph of description.split(/\n\s*\n/)) {
729
- if (paragraph.trim()) block.append(el("p", void 0, paragraph.trim()));
988
+ if (descriptionMarkdown) {
989
+ renderMarkdownDescription(block, descriptionMarkdown);
990
+ } else {
991
+ for (const paragraph of description.split(/\n\s*\n/)) {
992
+ if (paragraph.trim()) block.append(el("p", void 0, paragraph.trim()));
993
+ }
730
994
  }
731
995
  card.append(block);
732
996
  }
@@ -1438,6 +1702,7 @@ export {
1438
1702
  MINDOODB_APP_VALUE_TAG,
1439
1703
  MINDOODB_APP_WELL_KNOWN_DRAG_TYPES,
1440
1704
  MINIMAL_DRAG_PREVIEW_PNG,
1705
+ MindooDBAppAgentToolError,
1441
1706
  MindooDBAppDragInputError,
1442
1707
  MindooDBAppValue,
1443
1708
  MindooDBRichTextHandle,
@@ -1461,11 +1726,13 @@ export {
1461
1726
  isLaunchedByHaven,
1462
1727
  isMindooDBAppBundleHash,
1463
1728
  isMindooDBAppDragType,
1729
+ listingMarkdownToPlainText,
1464
1730
  matchHostShortcut,
1465
1731
  normalizeDragAccepts,
1466
1732
  normalizeDragPointerInput,
1467
1733
  normalizeDragStartInput,
1468
1734
  normalizeMindooDBAppBundlePath,
1735
+ parseListingMarkdown,
1469
1736
  queryDocuments,
1470
1737
  readMindooDBAppHostingMode,
1471
1738
  readMindooDBAppLaunchId,
@@ -1476,6 +1743,7 @@ export {
1476
1743
  resolveMindooDBAppListingAssetUrl,
1477
1744
  resolveMindooDBAppLocalizedText,
1478
1745
  sanitizeHostShortcutBindings,
1746
+ sanitizeListingHref,
1479
1747
  snapshotElementToPng,
1480
1748
  validateMindooDBAppBundleManifest,
1481
1749
  validateMindooDBAppDefinition,
@@ -1,4 +1,4 @@
1
- import { ay as MindooDBAppCreateViewNavigatorInput, cC as MindooDBAppViewNavigator, $ as MindooDBAppBridgeRpcRequest, b7 as MindooDBAppHostTheme, ag as MindooDBAppCapability, i as MindooDBAppDefinition, e as MindooDBAppBridge, cc as MindooDBAppSession, bE as MindooDBAppProposeAppInput, cK as MindooDBAppViewport, cw as MindooDBAppUiPreferences, bK as MindooDBAppQueryResult, cJ as MindooDBAppViewUpdateStats, X as MindooDBAppBridgePortMessage, bl as MindooDBAppLaunchContext, az as MindooDBAppDatabaseInfo, aI as MindooDBAppDocumentApi, cE as MindooDBAppViewNavigatorOpenOptions, A as MindooDBAppAttachmentApi, b8 as MindooDBAppIdentityApi, cs as MindooDBAppTimestampApi, aG as MindooDBAppDirectoryApi, k as MindooDBAppDocument, b4 as MindooDBAppFulltextSetup, b3 as MindooDBAppExtractionSetup, ck as MindooDBAppSummarySetup, bF as MindooDBAppProposeAppResult, c as MindooDBAppDragStartResult, aZ as MindooDBAppDragProfile } from '../types-CsKw_niD.js';
1
+ import { aG as MindooDBAppCreateViewNavigatorInput, cM as MindooDBAppViewNavigator, a7 as MindooDBAppBridgeRpcRequest, bf as MindooDBAppHostTheme, ao as MindooDBAppCapability, j as MindooDBAppDefinition, G as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cm as MindooDBAppSession, bO as MindooDBAppProposeAppInput, cU as MindooDBAppViewport, cG as MindooDBAppUiPreferences, bU as MindooDBAppQueryResult, cT as MindooDBAppViewUpdateStats, a3 as MindooDBAppBridgePortMessage, aH as MindooDBAppDatabaseInfo, $ as MindooDBAppBridgeErrorPayload, bt as MindooDBAppLaunchContext, aQ as MindooDBAppDocumentApi, cO as MindooDBAppViewNavigatorOpenOptions, bg as MindooDBAppIdentityApi, cC as MindooDBAppTimestampApi, aO as MindooDBAppDirectoryApi, l as MindooDBAppDocument, bc as MindooDBAppFulltextSetup, bb as MindooDBAppExtractionSetup, cu as MindooDBAppSummarySetup, bP as MindooDBAppProposeAppResult, c as MindooDBAppDragStartResult, b5 as MindooDBAppDragProfile } from '../types-Dl70Tlfj.js';
2
2
  import * as mindoodb from 'mindoodb';
3
3
  import 'mindoodb-view-language';
4
4
 
@@ -31,12 +31,22 @@ declare function createEvaluatingViewNavigator(input: MindooDBAppCreateViewNavig
31
31
  * tenant and an install in a real Haven. It is never meant to be deployed where end
32
32
  * users can reach it.
33
33
  */
34
- /** A scan the test host hands out on the app's next `attachments.scan()` call. */
34
+ /**
35
+ * A scan the test host hands out on the app's `attachments.scan()` call. With `bytes`
36
+ * the file is written to the document's attachments, as Haven's scanner does; without,
37
+ * only the attachment info is returned.
38
+ */
35
39
  interface BrowserTestHostScan {
36
40
  fileName: string;
37
41
  mimeType: "image/jpeg" | "image/png" | "application/pdf";
38
- size: number;
42
+ size?: number;
43
+ bytes?: Uint8Array;
39
44
  }
45
+ /**
46
+ * What a scan returns when no {@link BrowserTestHost.setNextScan} is pending: cancel,
47
+ * a generated sample page, or a fixed file.
48
+ */
49
+ type BrowserTestHostScanMode = "cancel" | "sample" | BrowserTestHostScan;
40
50
  /** What the test host recorded, in the order the app asked. */
41
51
  interface BrowserTestHostLog {
42
52
  notifications: Array<{
@@ -54,15 +64,22 @@ interface BrowserTestHostLog {
54
64
  databaseId: string;
55
65
  docId: string;
56
66
  result: "scanned" | "cancelled";
67
+ fileName?: string;
68
+ mimeType?: string;
69
+ size?: number;
57
70
  }>;
58
71
  focusRequests: number;
59
72
  requests: MindooDBAppBridgeRpcRequest[];
73
+ /** The answer to each request, by request id (result or error, duration). */
74
+ responses: Record<string, FakeBridgeRpcResponse>;
60
75
  }
61
76
  interface CreateBrowserTestHostOptions extends CreateFakeBridgeHostOptions {
62
77
  /** The iframe the app runs in. Its `src` is set by the host. */
63
78
  frame: HTMLIFrameElement;
64
79
  /** URL of the app page, relative to the test page or absolute. */
65
80
  appUrl: string;
81
+ /** What scans return while no `setNextScan()` is pending. Default `"cancel"`. */
82
+ scanMode?: BrowserTestHostScanMode;
66
83
  /** Called after every request, e.g. to re-render a control panel. */
67
84
  onChange?: () => void;
68
85
  }
@@ -70,30 +87,98 @@ interface BrowserTestHost {
70
87
  host: FakeBridgeHostController;
71
88
  log: BrowserTestHostLog;
72
89
  /**
73
- * Decides the outcome of the app's next `attachments.scan()`: a file description to
74
- * "scan", or `null` to act as if the user cancelled. Without a call, scans cancel.
90
+ * Decides the outcome of the app's next `attachments.scan()`: a file to "scan", or
91
+ * `null` to act as if the user cancelled. Later scans follow {@link setScanMode}.
75
92
  */
76
93
  setNextScan(scan: BrowserTestHostScan | null): void;
94
+ /** What scans return while no `setNextScan()` is pending. */
95
+ setScanMode(mode: BrowserTestHostScanMode): void;
96
+ getScanMode(): BrowserTestHostScanMode;
77
97
  setTheme(theme: MindooDBAppHostTheme): void;
78
98
  setHostFocus(focused: boolean): void;
99
+ /** Haven's language changed: the app gets `onLocaleChange`, no reload. */
100
+ setLocale(locale: string): void;
101
+ /**
102
+ * Grants a mapped database these capabilities and reloads the app, like Haven
103
+ * relaunching it after a permission change.
104
+ */
105
+ setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
106
+ /** Turns the capability checks on or off (`enforceCapabilities`). */
107
+ setEnforceCapabilities(enforce: boolean): void;
108
+ /** Replaces the usernames of the tenant directory (`directory.listUsers()`). */
109
+ setDirectoryUsers(users: readonly string[]): void;
79
110
  /** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
80
111
  reloadApp(): void;
81
112
  dispose(): void;
82
113
  }
114
+ /** Every capability Haven can grant, in the order the panel lists them. */
115
+ declare const ALL_CAPABILITIES: readonly MindooDBAppCapability[];
83
116
  /**
84
117
  * The capabilities Haven grants for the permissions a definition requests. `read` is
85
118
  * implied by the mapping, `write` is create plus update.
86
119
  */
87
120
  declare function capabilitiesForDefinitionPermissions(permissions: readonly string[] | undefined): MindooDBAppCapability[];
88
- /** Mock databases for every database a `haven-app.json` declares, granted as requested. */
89
- declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinition, "databases">, seed?: Record<string, MockMindooDBAppDatabaseDefinition["documents"]>): MockMindooDBAppDatabaseDefinition[];
121
+ /** Changes to one database of a definition, for testing other mappings than requested. */
122
+ interface MockDatabaseOverride {
123
+ title?: string;
124
+ /** Grant exactly these instead of what the definition requests. */
125
+ capabilities?: MindooDBAppCapability[];
126
+ /** `false`: the user did not map this database to the app. */
127
+ mapped?: boolean;
128
+ }
129
+ interface MockDatabasesFromDefinitionOptions {
130
+ /** By logical database id. */
131
+ overrides?: Record<string, MockDatabaseOverride>;
132
+ /**
133
+ * `"memory"` (default): attachments the app writes can be listed and read back.
134
+ * `"none"`: the mock's stateless default.
135
+ */
136
+ attachments?: "memory" | "none";
137
+ }
138
+ /**
139
+ * Mock databases for every database a `haven-app.json` declares, granted as requested
140
+ * unless `options.overrides` says otherwise.
141
+ */
142
+ declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinition, "databases">, seed?: Record<string, MockMindooDBAppDatabaseDefinition["documents"]>, options?: MockDatabasesFromDefinitionOptions): MockMindooDBAppDatabaseDefinition[];
143
+ /**
144
+ * Test-host settings from the page URL, so a scenario can be linked or scripted:
145
+ *
146
+ * - `db=<id>:<capability>,<capability>` grants exactly these; `db=<id>:none` unmaps it
147
+ * - `enforce=1` turns the capability checks on
148
+ * - `users=<count>` fills the directory with generated users
149
+ * - `locale=<tag>`, `theme=dark|light`
150
+ */
151
+ interface TestHostUrlSettings {
152
+ overrides: Record<string, MockDatabaseOverride>;
153
+ enforceCapabilities?: boolean;
154
+ directoryUserCount?: number;
155
+ locale?: string;
156
+ theme?: "dark" | "light";
157
+ }
158
+ declare function readTestHostUrlSettings(search: string): TestHostUrlSettings;
90
159
  declare function createBrowserTestHost(options: CreateBrowserTestHostOptions): BrowserTestHost;
91
160
  interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "frame" | "onChange"> {
92
161
  /** Element to render into. Defaults to `document.body`, which is cleared. */
93
162
  container?: HTMLElement;
94
163
  /** Heading of the control panel, e.g. the app label. */
95
164
  title?: string;
165
+ /** Locales offered in the panel's language switcher. */
166
+ locales?: ReadonlyArray<{
167
+ tag: string;
168
+ label: string;
169
+ }>;
170
+ /**
171
+ * Read `db=`, `enforce=`, `users=`, `locale=` and `theme=` from the page URL (see
172
+ * {@link readTestHostUrlSettings}) and keep the URL in sync with the panel. Default
173
+ * true. URL settings win over the options passed here.
174
+ */
175
+ urlSettings?: boolean;
96
176
  }
177
+ /** The locales Haven's UI offers; the default of the panel's language switcher. */
178
+ declare const TEST_HOST_LOCALES: ReadonlyArray<{
179
+ tag: string;
180
+ label: string;
181
+ }>;
97
182
  declare global {
98
183
  interface Window {
99
184
  /** Set by {@link mountHavenTestHost} so Playwright can script the host. */
@@ -101,12 +186,36 @@ declare global {
101
186
  }
102
187
  }
103
188
  /**
104
- * Renders the full test page: the app frame plus a control panel for theme, host focus
105
- * and the next scan, and a live log of notifications, previews, scans and requests.
189
+ * JSON for the request log: binary as size plus a hex preview, long strings and arrays
190
+ * cut, deep objects summarized.
191
+ */
192
+ declare function previewJson(value: unknown, maxString?: number, maxItems?: number, maxDepth?: number): string;
193
+ /**
194
+ * Renders the full test page: the app frame plus a control panel — theme, host focus,
195
+ * language, capabilities per database, directory users, the scanner — and a live log
196
+ * of notifications, previews, scans and requests (click an entry for details).
106
197
  * The returned host is also available as `window.__havenTestHost`.
107
198
  */
108
199
  declare function mountHavenTestHost(options: MountHavenTestHostOptions): BrowserTestHost;
109
200
 
201
+ /**
202
+ * In-memory attachments for mock databases: what an app writes can be listed and read
203
+ * back, like in Haven. The default mock attachment API stores nothing; the browser test
204
+ * host (`mockDatabasesFromDefinition`) uses this store instead.
205
+ */
206
+
207
+ declare function createMemoryAttachments(): Partial<MindooDBAppAttachmentApi>;
208
+
209
+ /**
210
+ * Capability checks for the mock databases, the way Haven enforces them: a call the
211
+ * database was not granted is rejected with a `forbidden` error. Opt in with
212
+ * `enforceCapabilities: true`; without it the mock only lists the capabilities.
213
+ */
214
+
215
+ declare class MockForbiddenError extends Error {
216
+ name: string;
217
+ }
218
+
110
219
  type MaybePromise<T> = T | Promise<T>;
111
220
  type MockViewApi = {
112
221
  create(input: MindooDBAppCreateViewNavigatorInput): MaybePromise<MindooDBAppViewNavigator>;
@@ -198,7 +307,27 @@ interface CreateMockMindooDBAppSessionOptions {
198
307
  proposeApp?: (input: MindooDBAppProposeAppInput) => MindooDBAppProposeAppResult;
199
308
  /** Initial contents of `session.storage`. */
200
309
  storage?: Record<string, string>;
310
+ /**
311
+ * Reject calls a database was not granted, like Haven does (a `forbidden` error;
312
+ * `documents.canCreate()` and friends answer `allowed: false`). Off by default so
313
+ * existing tests keep passing; turn it on to test how an app handles missing rights.
314
+ */
315
+ enforceCapabilities?: boolean;
316
+ /**
317
+ * Usernames `directory.listUsers()` returns on every database (Haven's directory is
318
+ * tenant-wide), e.g. `generateDirectoryUsers(120)`. A database's own
319
+ * `methods.directory` still wins.
320
+ */
321
+ directoryUsers?: readonly string[];
201
322
  }
323
+ /**
324
+ * Test usernames in Haven's canonical form: `CN=Test User 001/O=Test`, …
325
+ * Enough of them (e.g. 120) exercise the paging of `directory.listUsers({ … })`.
326
+ */
327
+ declare function generateDirectoryUsers(count: number, options?: {
328
+ prefix?: string;
329
+ organization?: string;
330
+ }): string[];
202
331
  interface MockMindooDBAppSessionController {
203
332
  bridge: MindooDBAppBridge;
204
333
  session: MindooDBAppSession;
@@ -209,6 +338,16 @@ interface MockMindooDBAppSessionController {
209
338
  proposedApps: MindooDBAppProposeAppInput[];
210
339
  listDatabases(): MindooDBAppDatabaseInfo[];
211
340
  setDatabases(definitions: MockMindooDBAppDatabaseDefinition[]): void;
341
+ /**
342
+ * Replaces the capabilities of one mapped database. Takes effect on the next call
343
+ * (with `enforceCapabilities`) and in the launch context; apps that read the
344
+ * capabilities once at start see the change after a reload.
345
+ */
346
+ setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
347
+ /** Turns the capability checks on or off (see `enforceCapabilities`). */
348
+ setEnforceCapabilities(enforce: boolean): void;
349
+ /** Replaces the directory's usernames (see `directoryUsers`). */
350
+ setDirectoryUsers(users: readonly string[]): void;
212
351
  emitThemeChange(theme: MindooDBAppHostTheme): void;
213
352
  emitViewportChange(viewport: MindooDBAppViewport): void;
214
353
  emitUiPreferencesChange(uiPreferences: MindooDBAppUiPreferences): void;
@@ -232,11 +371,23 @@ type FakeBridgePortMessageHandler = (message: Exclude<MindooDBAppBridgePortMessa
232
371
  host: FakeBridgeHostController;
233
372
  port: MessagePort;
234
373
  }) => MaybePromise<boolean | void>;
374
+ /** How the fake host answered one RPC request (see `onResponse`). */
375
+ type FakeBridgeRpcResponse = {
376
+ ok: true;
377
+ result: unknown;
378
+ durationMs: number;
379
+ } | {
380
+ ok: false;
381
+ error: MindooDBAppBridgeErrorPayload;
382
+ durationMs: number;
383
+ };
235
384
  interface CreateFakeBridgeHostOptions extends CreateMockMindooDBAppSessionOptions {
236
385
  windowMode?: "parent" | "opener";
237
386
  requestHandlers?: Record<string, FakeBridgeRequestHandler>;
238
387
  /** Called for every RPC request the app sends, before it is answered. */
239
388
  onRequest?: (request: MindooDBAppBridgeRpcRequest) => void;
389
+ /** Called with the answer to every RPC request, right after it was sent. */
390
+ onResponse?: (request: MindooDBAppBridgeRpcRequest, response: FakeBridgeRpcResponse) => void;
240
391
  onPortMessage?: FakeBridgePortMessageHandler;
241
392
  }
242
393
  interface FakeBridgeHostController {
@@ -272,7 +423,16 @@ interface FakeBridgeHostController {
272
423
  postPortMessage(message: MindooDBAppBridgePortMessage, transfer?: Transferable[]): void;
273
424
  setRequestHandler(method: string, handler: FakeBridgeRequestHandler): void;
274
425
  clearRequestHandler(method: string): void;
426
+ listDatabases(): MindooDBAppDatabaseInfo[];
427
+ /** See {@link MockMindooDBAppSessionController.setCapabilities}. */
428
+ setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
429
+ /** See {@link CreateMockMindooDBAppSessionOptions.enforceCapabilities}. */
430
+ setEnforceCapabilities(enforce: boolean): void;
431
+ getEnforceCapabilities(): boolean;
432
+ /** See {@link CreateMockMindooDBAppSessionOptions.directoryUsers}. */
433
+ setDirectoryUsers(users: readonly string[]): void;
434
+ getDirectoryUsers(): string[];
275
435
  }
276
436
  declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
277
437
 
278
- export { type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, loadMindoodbForTesting, mockDatabasesFromDefinition, mountHavenTestHost };
438
+ export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, MockForbiddenError, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings };