mindoodb-app-sdk 0.0.51 → 0.0.53

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
@@ -23,7 +23,7 @@ import {
23
23
  releaseMindooDBAppBridgeSessions,
24
24
  sanitizeHostShortcutBindings,
25
25
  snapshotElementToPng
26
- } from "./chunk-R76XXRJ6.js";
26
+ } from "./chunk-6NTVTL6G.js";
27
27
  import {
28
28
  MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
29
29
  MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
@@ -61,6 +61,9 @@ var MINDOODB_APP_DEFINITION_PERMISSIONS = [
61
61
  "proposeapps"
62
62
  ];
63
63
  var MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS = ["proposeapps"];
64
+ var MINDOODB_APP_LISTING_SUMMARY_MAX = 300;
65
+ var MINDOODB_APP_LISTING_DESCRIPTION_MAX = 4e3;
66
+ var MINDOODB_APP_LISTING_MAX_SCREENSHOTS = 8;
64
67
  var LOGICAL_DATABASE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
65
68
  function isPlainObject(value) {
66
69
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -200,6 +203,156 @@ function readDatabases(value, errors) {
200
203
  });
201
204
  return result.length ? result : void 0;
202
205
  }
206
+ function readLocalizedText(value, label, maxLength, errors) {
207
+ if (value === void 0 || value === null) {
208
+ return void 0;
209
+ }
210
+ if (typeof value === "string") {
211
+ const trimmed = value.trim();
212
+ if (trimmed.length > maxLength) {
213
+ errors.push(`${label} must not be longer than ${maxLength} characters.`);
214
+ return void 0;
215
+ }
216
+ return trimmed || void 0;
217
+ }
218
+ if (!isPlainObject(value)) {
219
+ errors.push(`${label} must be a string or an object of per-locale strings.`);
220
+ return void 0;
221
+ }
222
+ const result = {};
223
+ for (const [locale, text] of Object.entries(value)) {
224
+ if (typeof text !== "string") {
225
+ errors.push(`${label}.${locale} must be a string.`);
226
+ continue;
227
+ }
228
+ const trimmed = text.trim();
229
+ if (trimmed.length > maxLength) {
230
+ errors.push(`${label}.${locale} must not be longer than ${maxLength} characters.`);
231
+ continue;
232
+ }
233
+ if (trimmed) {
234
+ result[locale] = trimmed;
235
+ }
236
+ }
237
+ if (!Object.keys(result).length) {
238
+ return void 0;
239
+ }
240
+ if (!result.en) {
241
+ errors.push(`${label} must include an "en" entry as the fallback for other locales.`);
242
+ return void 0;
243
+ }
244
+ return result;
245
+ }
246
+ function readListingAssetPath(value, label, errors) {
247
+ if (value === void 0 || value === null) {
248
+ return void 0;
249
+ }
250
+ if (typeof value !== "string" || !value.trim()) {
251
+ errors.push(`${label} must be a non-empty string.`);
252
+ return void 0;
253
+ }
254
+ const trimmed = value.trim();
255
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed)?.[1]?.toLowerCase();
256
+ if (scheme && scheme !== "https") {
257
+ errors.push(`${label} must be a path on the app origin or an https: URL.`);
258
+ return void 0;
259
+ }
260
+ if (trimmed.startsWith("//")) {
261
+ errors.push(`${label} must be a path on the app origin or an https: URL.`);
262
+ return void 0;
263
+ }
264
+ return trimmed;
265
+ }
266
+ function readListing(value, errors) {
267
+ if (value === void 0 || value === null) {
268
+ return void 0;
269
+ }
270
+ if (!isPlainObject(value)) {
271
+ errors.push("App definition listing must be an object.");
272
+ return void 0;
273
+ }
274
+ const listing = {};
275
+ const summary = readLocalizedText(
276
+ value.summary,
277
+ "App definition listing.summary",
278
+ MINDOODB_APP_LISTING_SUMMARY_MAX,
279
+ errors
280
+ );
281
+ if (summary) listing.summary = summary;
282
+ const description = readLocalizedText(
283
+ value.description,
284
+ "App definition listing.description",
285
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
286
+ errors
287
+ );
288
+ if (description) listing.description = description;
289
+ const icon = readListingAssetPath(value.icon, "App definition listing.icon", errors);
290
+ if (icon) listing.icon = icon;
291
+ if (value.screenshots !== void 0 && value.screenshots !== null) {
292
+ if (!Array.isArray(value.screenshots)) {
293
+ errors.push("App definition listing.screenshots must be an array.");
294
+ } else if (value.screenshots.length > MINDOODB_APP_LISTING_MAX_SCREENSHOTS) {
295
+ errors.push(
296
+ `App definition listing.screenshots must not list more than ${MINDOODB_APP_LISTING_MAX_SCREENSHOTS} images.`
297
+ );
298
+ } else {
299
+ const screenshots = [];
300
+ value.screenshots.forEach((entry, index) => {
301
+ const label = `App definition listing.screenshots[${index}]`;
302
+ const raw = typeof entry === "string" ? { file: entry } : entry;
303
+ if (!isPlainObject(raw)) {
304
+ errors.push(`${label} must be a path or an object with a file.`);
305
+ return;
306
+ }
307
+ const file = readListingAssetPath(raw.file, `${label}.file`, errors);
308
+ if (!file) {
309
+ return;
310
+ }
311
+ const caption = readLocalizedText(raw.caption, `${label}.caption`, MINDOODB_APP_LISTING_SUMMARY_MAX, errors);
312
+ screenshots.push(caption ? { file, caption } : { file });
313
+ });
314
+ if (screenshots.length) listing.screenshots = screenshots;
315
+ }
316
+ }
317
+ if (value.publisher !== void 0 && value.publisher !== null) {
318
+ const publisher = isPlainObject(value.publisher) ? value.publisher : {};
319
+ const name = typeof publisher.name === "string" ? publisher.name.trim() : "";
320
+ if (!name) {
321
+ errors.push("App definition listing.publisher must be an object with a non-empty name.");
322
+ } else {
323
+ const url = readListingAssetPath(publisher.url, "App definition listing.publisher.url", errors);
324
+ if (url && !/^https:\/\//i.test(url)) {
325
+ errors.push("App definition listing.publisher.url must be an absolute https: URL.");
326
+ } else {
327
+ listing.publisher = url ? { name, url } : { name };
328
+ }
329
+ }
330
+ }
331
+ return Object.keys(listing).length ? listing : void 0;
332
+ }
333
+ function resolveMindooDBAppLocalizedText(text, locale) {
334
+ if (text === void 0) {
335
+ return "";
336
+ }
337
+ if (typeof text === "string") {
338
+ return text;
339
+ }
340
+ const normalized = locale.trim();
341
+ const language = normalized.split(/[-_]/)[0]?.toLowerCase() ?? "";
342
+ return text[normalized] ?? text[normalized.toLowerCase()] ?? (language ? text[language] : void 0) ?? text.en;
343
+ }
344
+ function resolveMindooDBAppListingAssetUrl(path, originUrl) {
345
+ try {
346
+ const base = new URL(originUrl.endsWith("/") ? originUrl : `${originUrl}/`);
347
+ const resolved = new URL(path, base);
348
+ if (resolved.protocol !== "https:" && resolved.protocol !== base.protocol) {
349
+ return null;
350
+ }
351
+ return resolved.toString();
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
203
356
  function validateMindooDBAppDefinition(raw) {
204
357
  const errors = [];
205
358
  if (!isPlainObject(raw)) {
@@ -251,6 +404,7 @@ function validateMindooDBAppDefinition(raw) {
251
404
  const allowGeolocation = readOptionalBoolean(raw, "allowGeolocation", errors);
252
405
  const allowWebRtc = readOptionalBoolean(raw, "allowWebRtc", errors);
253
406
  const allowWorkers = readOptionalBoolean(raw, "allowWorkers", errors);
407
+ const listing = readListing(raw.listing, errors);
254
408
  if (defaultLaunchDatabaseId && databases && !databases.some((database) => database.logicalDatabaseId === defaultLaunchDatabaseId)) {
255
409
  errors.push(
256
410
  `App definition defaultLaunchDatabaseId "${defaultLaunchDatabaseId}" is not one of the declared databases.`
@@ -279,7 +433,8 @@ function validateMindooDBAppDefinition(raw) {
279
433
  permissions,
280
434
  launchParameters,
281
435
  defaultLaunchDatabaseId,
282
- databases
436
+ databases,
437
+ listing
283
438
  },
284
439
  errors: []
285
440
  };
@@ -389,6 +544,22 @@ function expandAbbreviatedName(value) {
389
544
 
390
545
  // src/client/hosting.ts
391
546
  var MINDOODB_APP_HOSTING_QUERY_PARAM = "mindoodbAppHosting";
547
+ var MINDOODB_APP_LAUNCH_ID_QUERY_PARAM = "mindoodbAppLaunchId";
548
+ function readMindooDBAppLaunchId(search) {
549
+ const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
550
+ if (!raw) {
551
+ return null;
552
+ }
553
+ try {
554
+ const launchId = new URLSearchParams(raw).get(MINDOODB_APP_LAUNCH_ID_QUERY_PARAM)?.trim();
555
+ return launchId || null;
556
+ } catch {
557
+ return null;
558
+ }
559
+ }
560
+ function isLaunchedByHaven(search) {
561
+ return readMindooDBAppLaunchId(search) !== null;
562
+ }
392
563
  function readMindooDBAppHostingMode(search) {
393
564
  const raw = search ?? (typeof window === "undefined" ? "" : window.location.search);
394
565
  if (!raw) {
@@ -404,6 +575,195 @@ function isHostedBundleRuntime(search) {
404
575
  return readMindooDBAppHostingMode(search) === "hosted";
405
576
  }
406
577
 
578
+ // src/landing.ts
579
+ var DEFAULT_HAVEN_URL = "https://haven.mindoodb.com";
580
+ var APP_ICON_FALLBACKS = ["appicon.png", "appicon.webp", "appicon.svg"];
581
+ var STRINGS = {
582
+ en: {
583
+ eyebrow: "App for MindooDB Haven",
584
+ cta: "Open in MindooDB Haven",
585
+ ctaHint: "Haven sets up your encrypted workspace and installs {label}. Already using Haven? Then the app is simply added.",
586
+ from: "from {origin}",
587
+ by: "by {publisher}",
588
+ screenshots: "Screenshots",
589
+ footer: "{label} runs inside MindooDB Haven, which keeps its data end-to-end encrypted and local-first. Opening this address on its own shows only this page.",
590
+ learnMore: "What is MindooDB?",
591
+ fallbackSummary: "This app runs inside MindooDB Haven."
592
+ },
593
+ de: {
594
+ eyebrow: "App f\xFCr MindooDB Haven",
595
+ cta: "In MindooDB Haven \xF6ffnen",
596
+ ctaHint: "Haven richtet deine verschl\xFCsselte Umgebung ein und installiert {label}. Du nutzt Haven schon? Dann wird die App einfach hinzugef\xFCgt.",
597
+ from: "von {origin}",
598
+ by: "von {publisher}",
599
+ screenshots: "Screenshots",
600
+ footer: "{label} l\xE4uft in MindooDB Haven, das ihre Daten Ende-zu-Ende-verschl\xFCsselt und lokal h\xE4lt. Direkt aufgerufen zeigt diese Adresse nur diese Seite.",
601
+ learnMore: "Was ist MindooDB?",
602
+ fallbackSummary: "Diese App l\xE4uft in MindooDB Haven."
603
+ }
604
+ };
605
+ function stringsFor(locale) {
606
+ return locale.toLowerCase().startsWith("de") ? STRINGS.de : STRINGS.en;
607
+ }
608
+ function format(template, values) {
609
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
610
+ }
611
+ function resolveCurrentHavenAppUrl(href = window.location.href) {
612
+ const url = new URL(href);
613
+ url.search = "";
614
+ url.hash = "";
615
+ url.pathname = url.pathname.replace(/\/index\.html?$/i, "/");
616
+ return url.toString();
617
+ }
618
+ function buildHavenAppInstallUrl(appUrl, havenUrl = DEFAULT_HAVEN_URL) {
619
+ const url = new URL(havenUrl);
620
+ url.searchParams.set("app", appUrl);
621
+ return url.toString();
622
+ }
623
+ async function loadDefinition(appUrl) {
624
+ try {
625
+ const definitionUrl = new URL(MINDOODB_APP_DEFINITION_FILE_NAME, appUrl.endsWith("/") ? appUrl : `${appUrl}/`);
626
+ const response = await fetch(definitionUrl, { headers: { Accept: "application/json" } });
627
+ if (!response.ok) {
628
+ return null;
629
+ }
630
+ return validateMindooDBAppDefinition(await response.json()).definition;
631
+ } catch {
632
+ return null;
633
+ }
634
+ }
635
+ function el(tag, className, text) {
636
+ const node = document.createElement(tag);
637
+ if (className) node.className = className;
638
+ if (text !== void 0) node.textContent = text;
639
+ return node;
640
+ }
641
+ var STYLE_ID = "mdb-landing-style";
642
+ var STYLE = `
643
+ .mdb-landing{--bg:#f4f6fb;--card:#fff;--text:#172033;--muted:#5b6478;--line:#e2e6ef;--accent:#1f3a8a;--accent-text:#fff;
644
+ min-height:100vh;box-sizing:border-box;margin:0;padding:48px 16px;background:var(--bg);color:var(--text);
645
+ font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
646
+ @media (prefers-color-scheme:dark){.mdb-landing{--bg:#0e1320;--card:#161d2e;--text:#e8ecf5;--muted:#9aa3b8;--line:#263049;--accent:#8fb0ff;--accent-text:#0e1320}}
647
+ .mdb-landing *{box-sizing:border-box}
648
+ .mdb-landing__card{max-width:880px;margin:0 auto;background:var(--card);border:1px solid var(--line);border-radius:24px;padding:40px;box-shadow:0 20px 60px rgba(15,23,42,.08)}
649
+ .mdb-landing__head{display:flex;gap:24px;align-items:center}
650
+ .mdb-landing__icon{width:96px;height:96px;border-radius:22px;object-fit:cover;flex:none;box-shadow:0 10px 30px rgba(15,23,42,.18)}
651
+ .mdb-landing__eyebrow{margin:0 0 4px;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
652
+ .mdb-landing__title{margin:0;font-size:clamp(26px,4vw,36px);line-height:1.15}
653
+ .mdb-landing__meta{margin:6px 0 0;color:var(--muted);font-size:14px}
654
+ .mdb-landing__summary{margin:28px 0 0;font-size:19px}
655
+ .mdb-landing__cta{display:flex;flex-wrap:wrap;gap:16px;align-items:center;margin:28px 0 0}
656
+ .mdb-landing__button{display:inline-block;padding:14px 22px;border-radius:12px;background:var(--accent);color:var(--accent-text);font-weight:600;text-decoration:none}
657
+ .mdb-landing__button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
658
+ .mdb-landing__hint{margin:0;max-width:420px;color:var(--muted);font-size:14px}
659
+ .mdb-landing__description{margin:32px 0 0}
660
+ .mdb-landing__description p{margin:0 0 12px}
661
+ .mdb-landing__shots-title{margin:32px 0 12px;font-size:15px;color:var(--muted);font-weight:600}
662
+ .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
+ .mdb-landing__shot{margin:0;scroll-snap-align:start}
664
+ .mdb-landing__shot img{width:100%;border-radius:14px;border:1px solid var(--line);display:block}
665
+ .mdb-landing__shot figcaption{margin-top:8px;font-size:13px;color:var(--muted)}
666
+ .mdb-landing__footer{margin:36px 0 0;padding-top:20px;border-top:1px solid var(--line);color:var(--muted);font-size:13px}
667
+ .mdb-landing__footer a{color:inherit}
668
+ @media (max-width:560px){.mdb-landing{padding:16px}.mdb-landing__card{padding:24px;border-radius:18px}.mdb-landing__head{flex-direction:column;align-items:flex-start}.mdb-landing__icon{width:72px;height:72px}}
669
+ `;
670
+ async function renderHavenAppLandingPage(options = {}) {
671
+ const appUrl = options.appUrl ?? resolveCurrentHavenAppUrl();
672
+ const locale = options.locale ?? (typeof navigator === "undefined" ? "en" : navigator.language || "en");
673
+ const strings = stringsFor(locale);
674
+ const definition = options.definition ?? await loadDefinition(appUrl);
675
+ const installUrl = buildHavenAppInstallUrl(appUrl, options.havenUrl);
676
+ const listing = definition?.listing;
677
+ const label = definition?.label ?? (document.title || new URL(appUrl).host);
678
+ const origin = new URL(appUrl).host;
679
+ if (!document.getElementById(STYLE_ID)) {
680
+ const style = el("style");
681
+ style.id = STYLE_ID;
682
+ style.textContent = STYLE;
683
+ document.head.append(style);
684
+ }
685
+ document.title = label;
686
+ const root = el("main", "mdb-landing");
687
+ root.dataset.testid = "haven-app-landing";
688
+ const card = el("article", "mdb-landing__card");
689
+ root.append(card);
690
+ const head = el("header", "mdb-landing__head");
691
+ const iconCandidates = (listing?.icon ? [listing.icon] : [...APP_ICON_FALLBACKS]).map((path) => resolveMindooDBAppListingAssetUrl(path, appUrl)).filter((url) => Boolean(url));
692
+ if (iconCandidates.length) {
693
+ const icon = el("img", "mdb-landing__icon");
694
+ let next = 0;
695
+ icon.alt = "";
696
+ icon.addEventListener("error", () => {
697
+ next += 1;
698
+ if (next < iconCandidates.length) {
699
+ icon.src = iconCandidates[next];
700
+ } else {
701
+ icon.remove();
702
+ }
703
+ });
704
+ icon.src = iconCandidates[0];
705
+ head.append(icon);
706
+ }
707
+ const titles = el("div");
708
+ titles.append(el("p", "mdb-landing__eyebrow", strings.eyebrow), el("h1", "mdb-landing__title", label));
709
+ const meta = [format(strings.from, { origin })];
710
+ if (listing?.publisher) {
711
+ meta.unshift(format(strings.by, { publisher: listing.publisher.name }));
712
+ }
713
+ titles.append(el("p", "mdb-landing__meta", meta.join(" \xB7 ")));
714
+ head.append(titles);
715
+ card.append(head);
716
+ const summary = resolveMindooDBAppLocalizedText(listing?.summary, locale) || definition?.description || strings.fallbackSummary;
717
+ card.append(el("p", "mdb-landing__summary", summary));
718
+ const cta = el("div", "mdb-landing__cta");
719
+ const button = el("a", "mdb-landing__button", strings.cta);
720
+ button.href = installUrl;
721
+ button.rel = "noopener";
722
+ button.dataset.testid = "haven-app-landing-install";
723
+ cta.append(button, el("p", "mdb-landing__hint", format(strings.ctaHint, { label })));
724
+ card.append(cta);
725
+ const description = resolveMindooDBAppLocalizedText(listing?.description, locale);
726
+ if (description) {
727
+ 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()));
730
+ }
731
+ card.append(block);
732
+ }
733
+ const screenshots = (listing?.screenshots ?? []).map((shot) => ({
734
+ url: resolveMindooDBAppListingAssetUrl(shot.file, appUrl),
735
+ caption: resolveMindooDBAppLocalizedText(shot.caption, locale)
736
+ })).filter((shot) => Boolean(shot.url));
737
+ if (screenshots.length) {
738
+ card.append(el("h2", "mdb-landing__shots-title", strings.screenshots));
739
+ const strip = el("div", "mdb-landing__shots");
740
+ for (const shot of screenshots) {
741
+ const figure = el("figure", "mdb-landing__shot");
742
+ const image = el("img");
743
+ image.src = shot.url;
744
+ image.alt = shot.caption;
745
+ image.loading = "lazy";
746
+ figure.append(image);
747
+ if (shot.caption) figure.append(el("figcaption", void 0, shot.caption));
748
+ strip.append(figure);
749
+ }
750
+ card.append(strip);
751
+ }
752
+ const footer = el("footer", "mdb-landing__footer");
753
+ footer.append(format(strings.footer, { label }), " ");
754
+ const learnMore = el("a", void 0, strings.learnMore);
755
+ learnMore.href = "https://mindoodb.com";
756
+ learnMore.rel = "noopener";
757
+ footer.append(learnMore);
758
+ card.append(footer);
759
+ const container = options.container ?? document.body;
760
+ container.replaceChildren(root);
761
+ if (container === document.body) {
762
+ document.body.style.margin = "0";
763
+ }
764
+ return { definition, installUrl };
765
+ }
766
+
407
767
  // src/client/havenStorageShim.ts
408
768
  var DEFAULT_QUOTA_BYTES = 256 * 1024;
409
769
  var DEFAULT_MAX_VALUE_BYTES = 64 * 1024;
@@ -731,11 +1091,11 @@ var MindooDBRichTextHandle = class {
731
1091
  this.pollTimer = null;
732
1092
  }
733
1093
  }
734
- reconcile(document, snapshot) {
1094
+ reconcile(document2, snapshot) {
735
1095
  const previous = JSON.stringify(this.spansInternal);
736
1096
  this.spansInternal = cloneSpans(snapshot.spans);
737
1097
  this.baseMaterializedText = spansToPlainText(snapshot.spans);
738
- this.heads = snapshot.heads ?? document.heads ?? [];
1098
+ this.heads = snapshot.heads ?? document2.heads ?? [];
739
1099
  this.dirtyInternal = false;
740
1100
  const changed = JSON.stringify(this.spansInternal) !== previous;
741
1101
  if (changed) {
@@ -754,14 +1114,14 @@ var MindooDBRichTextHandle = class {
754
1114
  if (this.dirtyInternal) {
755
1115
  return false;
756
1116
  }
757
- const document = await this.options.database.documents.get(
1117
+ const document2 = await this.options.database.documents.get(
758
1118
  this.options.document.id
759
1119
  );
760
- if (!document) {
1120
+ if (!document2) {
761
1121
  return false;
762
1122
  }
763
1123
  const snapshot = await this.refresh();
764
- return this.reconcile(document, snapshot);
1124
+ return this.reconcile(document2, snapshot);
765
1125
  }
766
1126
  async flush(updateSpansConfig) {
767
1127
  if (!this.dirtyInternal) {
@@ -778,7 +1138,7 @@ var MindooDBRichTextHandle = class {
778
1138
  const localSpansAtFlush = cloneSpans(this.spansInternal);
779
1139
  const localTextAtFlush = spansToPlainText(localSpansAtFlush);
780
1140
  const textChanged = localTextAtFlush !== this.baseMaterializedText;
781
- const document = textChanged ? await this.flushTextSteps(localTextAtFlush) : await this.options.database.documents.update(
1141
+ const document2 = textChanged ? await this.flushTextSteps(localTextAtFlush) : await this.options.database.documents.update(
782
1142
  this.options.document.id,
783
1143
  {
784
1144
  richText: [{
@@ -796,13 +1156,13 @@ var MindooDBRichTextHandle = class {
796
1156
  const reconciled = JSON.stringify(snapshot.spans) !== JSON.stringify(localSpansAtFlush);
797
1157
  this.spansInternal = cloneSpans(snapshot.spans);
798
1158
  this.baseMaterializedText = spansToPlainText(snapshot.spans);
799
- this.heads = snapshot.heads ?? document.heads ?? [];
1159
+ this.heads = snapshot.heads ?? document2.heads ?? [];
800
1160
  this.dirtyInternal = false;
801
1161
  if (reconciled) {
802
1162
  this.emitChange();
803
1163
  }
804
1164
  return {
805
- document,
1165
+ document: document2,
806
1166
  snapshot,
807
1167
  reconciled
808
1168
  };
@@ -940,11 +1300,11 @@ var MindooDBTextBuffer = class {
940
1300
  * Returns whether the text value changed. Use this after an external refresh
941
1301
  * or after receiving a document update through another app-level mechanism.
942
1302
  */
943
- reconcile(document) {
944
- const nextValue = readTextAtPath(document.data, this.options.path);
1303
+ reconcile(document2) {
1304
+ const nextValue = readTextAtPath(document2.data, this.options.path);
945
1305
  const changed = nextValue !== this.valueInternal;
946
1306
  this.valueInternal = nextValue;
947
- this.heads = document.heads ? [...document.heads] : [];
1307
+ this.heads = document2.heads ? [...document2.heads] : [];
948
1308
  this.pendingEdits = [];
949
1309
  return changed;
950
1310
  }
@@ -966,20 +1326,20 @@ var MindooDBTextBuffer = class {
966
1326
  };
967
1327
  }
968
1328
  const localValueAtFlush = this.valueInternal;
969
- const document = await this.options.database.documents.update(this.options.document.id, {
1329
+ const document2 = await this.options.database.documents.update(this.options.document.id, {
970
1330
  text: [{
971
1331
  path: this.options.path,
972
1332
  baseHeads: this.heads,
973
1333
  edits: this.pendingEdits.map((edit) => ({ ...edit }))
974
1334
  }]
975
1335
  });
976
- const nextValue = readTextAtPath(document.data, this.options.path);
1336
+ const nextValue = readTextAtPath(document2.data, this.options.path);
977
1337
  const reconciled = nextValue !== localValueAtFlush;
978
1338
  this.valueInternal = nextValue;
979
- this.heads = document.heads ? [...document.heads] : [];
1339
+ this.heads = document2.heads ? [...document2.heads] : [];
980
1340
  this.pendingEdits = [];
981
1341
  return {
982
- document,
1342
+ document: document2,
983
1343
  value: this.valueInternal,
984
1344
  reconciled
985
1345
  };
@@ -1055,6 +1415,7 @@ export {
1055
1415
  DEFAULT_DRAG_LONG_PRESS_MS,
1056
1416
  DEFAULT_DRAG_POINTER_THRESHOLD_PX,
1057
1417
  DEFAULT_HAVEN_HOST_SHORTCUTS,
1418
+ DEFAULT_HAVEN_URL,
1058
1419
  MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
1059
1420
  MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
1060
1421
  MINDOODB_APP_BUNDLE_MANIFEST_FORMAT,
@@ -1065,6 +1426,10 @@ export {
1065
1426
  MINDOODB_APP_DEFINITION_REGISTRATION_PERMISSIONS,
1066
1427
  MINDOODB_APP_DEFINITION_VERSION,
1067
1428
  MINDOODB_APP_HOSTING_QUERY_PARAM,
1429
+ MINDOODB_APP_LAUNCH_ID_QUERY_PARAM,
1430
+ MINDOODB_APP_LISTING_DESCRIPTION_MAX,
1431
+ MINDOODB_APP_LISTING_MAX_SCREENSHOTS,
1432
+ MINDOODB_APP_LISTING_SUMMARY_MAX,
1068
1433
  MINDOODB_APP_MAX_DRAG_PAYLOAD_BYTES,
1069
1434
  MINDOODB_APP_MAX_DRAG_PREVIEW_BYTES,
1070
1435
  MINDOODB_APP_MAX_DRAG_PREVIEW_EDGE,
@@ -1080,6 +1445,7 @@ export {
1080
1445
  abbreviateCanonicalName,
1081
1446
  applyHostedDocumentOverscrollContain,
1082
1447
  bindDragSource,
1448
+ buildHavenAppInstallUrl,
1083
1449
  buildMindooDBAppBundleContentHashInput,
1084
1450
  canPreviewAttachment,
1085
1451
  createMindooDBAppBridge,
@@ -1092,6 +1458,7 @@ export {
1092
1458
  installHostShortcutCapture,
1093
1459
  isHostShortcutAction,
1094
1460
  isHostedBundleRuntime,
1461
+ isLaunchedByHaven,
1095
1462
  isMindooDBAppBundleHash,
1096
1463
  isMindooDBAppDragType,
1097
1464
  matchHostShortcut,
@@ -1101,8 +1468,13 @@ export {
1101
1468
  normalizeMindooDBAppBundlePath,
1102
1469
  queryDocuments,
1103
1470
  readMindooDBAppHostingMode,
1471
+ readMindooDBAppLaunchId,
1104
1472
  releaseMindooDBAppBridgeSessions,
1473
+ renderHavenAppLandingPage,
1474
+ resolveCurrentHavenAppUrl,
1105
1475
  resolveMindooDBAppDefinitionUrl,
1476
+ resolveMindooDBAppListingAssetUrl,
1477
+ resolveMindooDBAppLocalizedText,
1106
1478
  sanitizeHostShortcutBindings,
1107
1479
  snapshotElementToPng,
1108
1480
  validateMindooDBAppBundleManifest,
@@ -1,4 +1,4 @@
1
- import { au as MindooDBAppCreateViewNavigatorInput, cw as MindooDBAppViewNavigator, bg as MindooDBAppLaunchContext, av as MindooDBAppDatabaseInfo, aD as MindooDBAppDocumentApi, cy as MindooDBAppViewNavigatorOpenOptions, w as MindooDBAppAttachmentApi, b3 as MindooDBAppIdentityApi, cm as MindooDBAppTimestampApi, aB as MindooDBAppDirectoryApi, j as MindooDBAppDocument, a$ as MindooDBAppFulltextSetup, a_ as MindooDBAppExtractionSetup, ce as MindooDBAppSummarySetup, by as MindooDBAppProposeAppInput, bz as MindooDBAppProposeAppResult, e as MindooDBAppBridge, c6 as MindooDBAppSession, X as MindooDBAppBridgeRpcRequest, b2 as MindooDBAppHostTheme, cE as MindooDBAppViewport, cq as MindooDBAppUiPreferences, bE as MindooDBAppQueryResult, cD as MindooDBAppViewUpdateStats, T as MindooDBAppBridgePortMessage, c as MindooDBAppDragStartResult, aU as MindooDBAppDragProfile } from '../types-BLyV6Ejy.js';
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';
2
2
  import * as mindoodb from 'mindoodb';
3
3
  import 'mindoodb-view-language';
4
4
 
@@ -21,6 +21,92 @@ declare function loadMindoodbForTesting(): Promise<MindoodbVirtualViews | null>;
21
21
  */
22
22
  declare function createEvaluatingViewNavigator(input: MindooDBAppCreateViewNavigatorInput, documents: EvaluatingViewDocument[]): Promise<MindooDBAppViewNavigator | null>;
23
23
 
24
+ /**
25
+ * A stand-in Haven for a real browser: the app runs in an iframe, exactly as Haven frames
26
+ * it, and talks to the same in-memory mock host the Vitest helpers use.
27
+ *
28
+ * This is what an app's test URL (`/__haven-test/` in the starter) mounts, so Playwright
29
+ * and a developer's own eyes see the real app — real bridge handshake, real DOM — with
30
+ * seeded data and scriptable host services, and without setting up an identity, a
31
+ * tenant and an install in a real Haven. It is never meant to be deployed where end
32
+ * users can reach it.
33
+ */
34
+ /** A scan the test host hands out on the app's next `attachments.scan()` call. */
35
+ interface BrowserTestHostScan {
36
+ fileName: string;
37
+ mimeType: "image/jpeg" | "image/png" | "application/pdf";
38
+ size: number;
39
+ }
40
+ /** What the test host recorded, in the order the app asked. */
41
+ interface BrowserTestHostLog {
42
+ notifications: Array<{
43
+ id?: string;
44
+ severity?: string;
45
+ text?: string;
46
+ durationMs?: number;
47
+ }>;
48
+ previews: Array<{
49
+ databaseId: string;
50
+ docId: string;
51
+ attachmentName: string;
52
+ }>;
53
+ scans: Array<{
54
+ databaseId: string;
55
+ docId: string;
56
+ result: "scanned" | "cancelled";
57
+ }>;
58
+ focusRequests: number;
59
+ requests: MindooDBAppBridgeRpcRequest[];
60
+ }
61
+ interface CreateBrowserTestHostOptions extends CreateFakeBridgeHostOptions {
62
+ /** The iframe the app runs in. Its `src` is set by the host. */
63
+ frame: HTMLIFrameElement;
64
+ /** URL of the app page, relative to the test page or absolute. */
65
+ appUrl: string;
66
+ /** Called after every request, e.g. to re-render a control panel. */
67
+ onChange?: () => void;
68
+ }
69
+ interface BrowserTestHost {
70
+ host: FakeBridgeHostController;
71
+ log: BrowserTestHostLog;
72
+ /**
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.
75
+ */
76
+ setNextScan(scan: BrowserTestHostScan | null): void;
77
+ setTheme(theme: MindooDBAppHostTheme): void;
78
+ setHostFocus(focused: boolean): void;
79
+ /** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
80
+ reloadApp(): void;
81
+ dispose(): void;
82
+ }
83
+ /**
84
+ * The capabilities Haven grants for the permissions a definition requests. `read` is
85
+ * implied by the mapping, `write` is create plus update.
86
+ */
87
+ 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[];
90
+ declare function createBrowserTestHost(options: CreateBrowserTestHostOptions): BrowserTestHost;
91
+ interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "frame" | "onChange"> {
92
+ /** Element to render into. Defaults to `document.body`, which is cleared. */
93
+ container?: HTMLElement;
94
+ /** Heading of the control panel, e.g. the app label. */
95
+ title?: string;
96
+ }
97
+ declare global {
98
+ interface Window {
99
+ /** Set by {@link mountHavenTestHost} so Playwright can script the host. */
100
+ __havenTestHost?: BrowserTestHost;
101
+ }
102
+ }
103
+ /**
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.
106
+ * The returned host is also available as `window.__havenTestHost`.
107
+ */
108
+ declare function mountHavenTestHost(options: MountHavenTestHostOptions): BrowserTestHost;
109
+
24
110
  type MaybePromise<T> = T | Promise<T>;
25
111
  type MockViewApi = {
26
112
  create(input: MindooDBAppCreateViewNavigatorInput): MaybePromise<MindooDBAppViewNavigator>;
@@ -149,6 +235,8 @@ type FakeBridgePortMessageHandler = (message: Exclude<MindooDBAppBridgePortMessa
149
235
  interface CreateFakeBridgeHostOptions extends CreateMockMindooDBAppSessionOptions {
150
236
  windowMode?: "parent" | "opener";
151
237
  requestHandlers?: Record<string, FakeBridgeRequestHandler>;
238
+ /** Called for every RPC request the app sends, before it is answered. */
239
+ onRequest?: (request: MindooDBAppBridgeRpcRequest) => void;
152
240
  onPortMessage?: FakeBridgePortMessageHandler;
153
241
  }
154
242
  interface FakeBridgeHostController {
@@ -159,6 +247,13 @@ interface FakeBridgeHostController {
159
247
  /** Every `apps.propose` call the app under test made, in order. */
160
248
  readonly proposedApps: ReadonlyArray<MindooDBAppProposeAppInput>;
161
249
  install(): void;
250
+ /**
251
+ * Answers a `mindoodb-app:connect` handshake received some other way than through
252
+ * `install()` — in a real browser, from the app frame's `message` event
253
+ * (`host.acceptConnection(event.data, event.ports)`). Returns `false` for messages
254
+ * that are not a handshake for this launch.
255
+ */
256
+ acceptConnection(message: unknown, transfer?: readonly Transferable[]): boolean;
162
257
  dispose(): void;
163
258
  emitThemeChange(theme: MindooDBAppHostTheme): void;
164
259
  emitViewportChange(viewport: MindooDBAppViewport): void;
@@ -180,4 +275,4 @@ interface FakeBridgeHostController {
180
275
  }
181
276
  declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
182
277
 
183
- export { type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, createEvaluatingViewNavigator, createFakeBridgeHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, loadMindoodbForTesting };
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 };