@einblick/sdk 0.7.0 → 0.7.3

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.
Files changed (41) hide show
  1. package/README.md +13 -11
  2. package/dist/cli.js +1 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client.js +2 -2
  5. package/dist/client.js.map +1 -1
  6. package/dist/codegen.d.ts.map +1 -1
  7. package/dist/codegen.js +63 -11
  8. package/dist/codegen.js.map +1 -1
  9. package/dist/next/edit-boot.d.ts +5 -17
  10. package/dist/next/edit-boot.d.ts.map +1 -1
  11. package/dist/next/edit-boot.js +5 -25
  12. package/dist/next/edit-boot.js.map +1 -1
  13. package/dist/next/revalidate.d.ts +4 -2
  14. package/dist/next/revalidate.d.ts.map +1 -1
  15. package/dist/next/revalidate.js +46 -12
  16. package/dist/next/revalidate.js.map +1 -1
  17. package/dist/react/markers.d.ts.map +1 -1
  18. package/dist/react/markers.js +20 -17
  19. package/dist/react/markers.js.map +1 -1
  20. package/dist/react/provider.d.ts.map +1 -1
  21. package/dist/react/provider.js +11 -2
  22. package/dist/react/provider.js.map +1 -1
  23. package/dist/react/runtime.d.ts +3 -0
  24. package/dist/react/runtime.d.ts.map +1 -1
  25. package/dist/react/runtime.js +307 -95
  26. package/dist/react/runtime.js.map +1 -1
  27. package/dist/react/shared.d.ts.map +1 -1
  28. package/dist/react/shared.js +10 -10
  29. package/dist/react/shared.js.map +1 -1
  30. package/dist/react/types.d.ts +2 -1
  31. package/dist/react/types.d.ts.map +1 -1
  32. package/dist/react/types.js.map +1 -1
  33. package/package.json +3 -6
  34. package/dist/client.test.d.ts +0 -2
  35. package/dist/client.test.d.ts.map +0 -1
  36. package/dist/client.test.js +0 -342
  37. package/dist/client.test.js.map +0 -1
  38. package/dist/codegen.test.d.ts +0 -2
  39. package/dist/codegen.test.d.ts.map +0 -1
  40. package/dist/codegen.test.js +0 -219
  41. package/dist/codegen.test.js.map +0 -1
@@ -7,6 +7,7 @@ const EINBLICK_ACCENT_COLOR = `var(${EINBLICK_ACCENT_COLOR_VAR}, ${DEFAULT_EINBL
7
7
  const EINBLICK_ACCENT_TINT = `color-mix(in srgb, ${EINBLICK_ACCENT_COLOR} 12%, transparent)`;
8
8
  const EINBLICK_ACCENT_HOVER_COLOR = `color-mix(in srgb, ${EINBLICK_ACCENT_COLOR} 92%, black)`;
9
9
  const CONTROL_BAR_HEIGHT = 44;
10
+ const BRIDGE_CONNECTION_TIMEOUT_MS = 8000;
10
11
  function getBrandBadgeSvg(size) {
11
12
  return `
12
13
  <svg viewBox="0 0 40 40" width="${size}" height="${size}" aria-hidden="true" focusable="false">
@@ -67,11 +68,15 @@ function createDatabaseIconElement(size) {
67
68
  return svg;
68
69
  }
69
70
  function createRandomId() {
70
- if (typeof crypto !== 'undefined' &&
71
- typeof crypto.randomUUID === 'function') {
71
+ if (typeof crypto === 'undefined') {
72
+ throw new Error('Secure browser randomness is unavailable');
73
+ }
74
+ if (typeof crypto.randomUUID === 'function') {
72
75
  return crypto.randomUUID();
73
76
  }
74
- return Math.random().toString(36).slice(2);
77
+ const bytes = new Uint8Array(16);
78
+ crypto.getRandomValues(bytes);
79
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
75
80
  }
76
81
  function stopEvent(event) {
77
82
  event.preventDefault();
@@ -202,6 +207,14 @@ function injectRuntimeStyles() {
202
207
  cursor: pointer;
203
208
  }
204
209
 
210
+ [data-einblick-edit-controls][hidden],
211
+ [data-einblick-edit-button][hidden],
212
+ [data-einblick-collection-create-button][hidden],
213
+ [data-einblick-collection-create-wrapper][hidden],
214
+ [data-einblick-field-button][hidden] {
215
+ display: none !important;
216
+ }
217
+
205
218
  [data-einblick-edit-button] svg,
206
219
  [data-einblick-collection-create-button] svg,
207
220
  [data-einblick-field-button] svg {
@@ -457,12 +470,17 @@ class EinblickEditRuntime {
457
470
  callbacks;
458
471
  chrome;
459
472
  reserveBottomSpace;
473
+ persist;
460
474
  locale;
461
475
  messages;
462
476
  accentColor;
463
477
  originalRootAccentColor;
464
478
  bridgeNonce = createRandomId();
465
479
  session;
480
+ brokerId = null;
481
+ brokerReadyWaiters = new Set();
482
+ collectionCreateControls = new Map();
483
+ loginPreparing = false;
466
484
  state = {
467
485
  enabled: true,
468
486
  active: true,
@@ -500,6 +518,7 @@ class EinblickEditRuntime {
500
518
  currentInlineRestoreState = null;
501
519
  inlineSaveInFlight = null;
502
520
  editorBackdrop = null;
521
+ editorFrame = null;
503
522
  editorNonce = null;
504
523
  editorTarget = null;
505
524
  editorBinding = null;
@@ -517,6 +536,7 @@ class EinblickEditRuntime {
517
536
  this.chrome = options.chrome ?? 'bar';
518
537
  this.reserveBottomSpace =
519
538
  options.reserveBottomSpace ?? this.chrome === 'bar';
539
+ this.persist = options.persist ?? true;
520
540
  this.locale = resolveEinblickSdkLocale(options.locale);
521
541
  this.messages = getEinblickSdkMessages(this.locale);
522
542
  this.accentColor = normalizeAccentColor(options.accentColor);
@@ -673,12 +693,12 @@ class EinblickEditRuntime {
673
693
  this.removeNavigatorPanel(false);
674
694
  this.closeDrawer();
675
695
  this.restoreAccentColor();
696
+ for (const control of this.collectionCreateControls.values()) {
697
+ control.container.remove();
698
+ }
699
+ this.collectionCreateControls.clear();
676
700
  if (this.pendingLogin) {
677
- if (this.pendingLogin.closePollId !== null) {
678
- window.clearInterval(this.pendingLogin.closePollId);
679
- }
680
- this.pendingLogin.resolve(false);
681
- this.pendingLogin = null;
701
+ this.resolvePendingLogin(this.pendingLogin.nonce, false, true);
682
702
  }
683
703
  for (const pending of this.pendingInlineSaves.values()) {
684
704
  window.clearTimeout(pending.timeoutId);
@@ -690,6 +710,7 @@ class EinblickEditRuntime {
690
710
  pending.reject(new Error(this.messages.errors.editingInterrupted));
691
711
  }
692
712
  this.pendingInlineMediaSaves.clear();
713
+ this.resolveBrokerReadyWaiters(null);
693
714
  this.setState({
694
715
  enabled: true,
695
716
  active: false,
@@ -726,42 +747,123 @@ class EinblickEditRuntime {
726
747
  if (typeof window === 'undefined') {
727
748
  return false;
728
749
  }
750
+ if (this.pendingLogin || this.loginPreparing) {
751
+ return false;
752
+ }
753
+ this.loginPreparing = true;
729
754
  const returnTo = options?.returnTo ?? window.location.href;
730
755
  const loginNonce = createRandomId();
731
- const loginUrl = this.buildBridgeUrl({
732
- mode: 'login',
733
- nonce: loginNonce,
734
- returnTo,
735
- });
736
- if (this.pendingLogin) {
737
- return false;
756
+ const popupFeatures = 'popup=yes,width=680,height=820,resizable=yes,scrollbars=yes';
757
+ let popup = null;
758
+ try {
759
+ // Reserve the popup while a direct user gesture is still active. The
760
+ // secure bridge may need a moment to prepare its broker, and opening a
761
+ // window after that await is blocked by browsers with strict popup
762
+ // policies. Navigation happens only after the SDK has registered the
763
+ // exact popup, nonce, and message source below.
764
+ popup = window.open('about:blank', 'einblick-edit-login', popupFeatures);
738
765
  }
739
- return await new Promise((resolve) => {
740
- let popup = null;
741
- try {
742
- popup = window.open(loginUrl, 'einblick-edit-login', 'popup=yes,width=680,height=820,resizable=yes,scrollbars=yes');
766
+ catch {
767
+ popup = null;
768
+ }
769
+ try {
770
+ const brokerId = await this.waitForBrokerId();
771
+ if (!brokerId) {
772
+ try {
773
+ popup?.close();
774
+ }
775
+ catch { }
776
+ const message = this.messages.errors.bridgeNotConnected;
777
+ this.callbacks.onError?.(message);
778
+ return false;
743
779
  }
744
- catch {
780
+ const loginUrl = this.buildBridgeUrl({
781
+ mode: 'login',
782
+ nonce: loginNonce,
783
+ returnTo,
784
+ });
785
+ if (popup?.closed) {
745
786
  popup = null;
746
787
  }
788
+ if (!popup) {
789
+ try {
790
+ popup = window.open('about:blank', 'einblick-edit-login', popupFeatures);
791
+ }
792
+ catch {
793
+ popup = null;
794
+ }
795
+ }
747
796
  if (!popup) {
748
797
  window.location.assign(loginUrl);
749
- resolve(false);
750
- return;
798
+ return false;
751
799
  }
752
- popup.focus();
753
- const closePollId = window.setInterval(() => {
754
- if (!popup || popup.closed) {
755
- window.clearInterval(closePollId);
756
- if (this.pendingLogin?.nonce === loginNonce) {
757
- this.pendingLogin.resolve(false);
758
- this.pendingLogin = null;
800
+ const loginResult = new Promise((resolve) => {
801
+ const closePollId = window.setInterval(() => {
802
+ if (!popup || popup.closed) {
803
+ this.resolvePendingLogin(loginNonce, false);
759
804
  }
805
+ }, 400);
806
+ this.pendingLogin = { nonce: loginNonce, popup, resolve, closePollId };
807
+ });
808
+ try {
809
+ popup.location.assign(loginUrl);
810
+ popup.focus();
811
+ }
812
+ catch {
813
+ this.resolvePendingLogin(loginNonce, false, true);
814
+ window.location.assign(loginUrl);
815
+ return false;
816
+ }
817
+ return await loginResult;
818
+ }
819
+ finally {
820
+ this.loginPreparing = false;
821
+ }
822
+ }
823
+ resolvePendingLogin(nonce, result, closePopup = false) {
824
+ const pending = this.pendingLogin;
825
+ if (!pending || pending.nonce !== nonce) {
826
+ return false;
827
+ }
828
+ if (pending.closePollId !== null) {
829
+ window.clearInterval(pending.closePollId);
830
+ }
831
+ this.pendingLogin = null;
832
+ if (closePopup) {
833
+ try {
834
+ pending.popup?.close();
835
+ }
836
+ catch { }
837
+ }
838
+ pending.resolve(result);
839
+ return true;
840
+ }
841
+ waitForBrokerId() {
842
+ if (this.brokerId) {
843
+ return Promise.resolve(this.brokerId);
844
+ }
845
+ this.pingBridge();
846
+ return new Promise((resolve) => {
847
+ let settled = false;
848
+ const finish = (brokerId) => {
849
+ if (settled) {
850
+ return;
760
851
  }
761
- }, 400);
762
- this.pendingLogin = { nonce: loginNonce, popup, resolve, closePollId };
852
+ settled = true;
853
+ window.clearTimeout(timeoutId);
854
+ this.brokerReadyWaiters.delete(finish);
855
+ resolve(brokerId);
856
+ };
857
+ const timeoutId = window.setTimeout(() => finish(null), BRIDGE_CONNECTION_TIMEOUT_MS);
858
+ this.brokerReadyWaiters.add(finish);
763
859
  });
764
860
  }
861
+ resolveBrokerReadyWaiters(brokerId) {
862
+ for (const resolve of this.brokerReadyWaiters) {
863
+ resolve(brokerId);
864
+ }
865
+ this.brokerReadyWaiters.clear();
866
+ }
765
867
  openEditor(binding, element) {
766
868
  this.setBrandPopoverOpen(false);
767
869
  this.setBottomBarMenuOpen(false);
@@ -919,14 +1021,24 @@ class EinblickEditRuntime {
919
1021
  return;
920
1022
  }
921
1023
  if (data.source === EINBLICK_BRIDGE_SOURCE) {
1024
+ if (event.source !== this.bridgeFrame?.contentWindow) {
1025
+ return;
1026
+ }
922
1027
  this.handleBridgeMessage(data);
923
1028
  return;
924
1029
  }
925
1030
  if (data.source === EINBLICK_AUTH_SOURCE) {
1031
+ if (event.source !== this.pendingLogin?.popup) {
1032
+ return;
1033
+ }
926
1034
  this.handleAuthMessage(data);
927
1035
  return;
928
1036
  }
929
1037
  if (data.source === EINBLICK_EDITOR_SOURCE) {
1038
+ if (event.source !== this.editorFrame?.contentWindow &&
1039
+ event.source !== this.navigatorFrame?.contentWindow) {
1040
+ return;
1041
+ }
930
1042
  this.handleEditorMessage(data);
931
1043
  }
932
1044
  };
@@ -936,7 +1048,10 @@ class EinblickEditRuntime {
936
1048
  }
937
1049
  if (message.type === 'bridge-ready') {
938
1050
  this.clearBridgePing();
1051
+ this.brokerId = message.brokerId ?? this.brokerId;
1052
+ this.resolveBrokerReadyWaiters(this.brokerId);
939
1053
  if (message.authenticated) {
1054
+ this.setSession({ active: true, expiresAt: message.expiresAt });
940
1055
  this.setState({
941
1056
  enabled: true,
942
1057
  active: true,
@@ -964,6 +1079,26 @@ class EinblickEditRuntime {
964
1079
  }
965
1080
  return;
966
1081
  }
1082
+ if (message.type === 'broker-proof-result') {
1083
+ const pending = this.pendingLogin;
1084
+ if (!pending ||
1085
+ message.requestId !== pending.nonce ||
1086
+ message.brokerId !== this.brokerId) {
1087
+ return;
1088
+ }
1089
+ try {
1090
+ pending.popup?.postMessage({
1091
+ source: EINBLICK_AUTH_SOURCE,
1092
+ type: 'broker-proof-result',
1093
+ nonce: pending.nonce,
1094
+ requestId: message.requestId,
1095
+ brokerId: message.brokerId,
1096
+ ciphertext: message.ciphertext,
1097
+ }, this.appOrigin);
1098
+ }
1099
+ catch { }
1100
+ return;
1101
+ }
967
1102
  if (message.type === 'logout-result') {
968
1103
  const pending = this.pendingLogout;
969
1104
  if (!pending || pending.requestId !== message.requestId) {
@@ -1011,35 +1146,42 @@ class EinblickEditRuntime {
1011
1146
  if (!this.pendingLogin || message.nonce !== this.pendingLogin.nonce) {
1012
1147
  return;
1013
1148
  }
1014
- if (this.pendingLogin.closePollId !== null) {
1015
- window.clearInterval(this.pendingLogin.closePollId);
1149
+ if (message.type === 'broker-proof-request') {
1150
+ if (message.requestId !== this.pendingLogin.nonce ||
1151
+ message.brokerId !== this.brokerId ||
1152
+ !this.bridgeFrame?.contentWindow) {
1153
+ return;
1154
+ }
1155
+ try {
1156
+ this.bridgeFrame.contentWindow.postMessage({
1157
+ source: EINBLICK_PARENT_SOURCE,
1158
+ type: 'broker-proof-request',
1159
+ nonce: this.bridgeNonce,
1160
+ requestId: message.requestId,
1161
+ brokerId: message.brokerId,
1162
+ publicKey: message.publicKey,
1163
+ }, this.appOrigin);
1164
+ }
1165
+ catch { }
1166
+ return;
1016
1167
  }
1017
1168
  const pending = this.pendingLogin;
1018
- this.pendingLogin = null;
1019
1169
  if (message.ok) {
1020
- if (!message.sessionToken) {
1170
+ if (!message.brokerId || message.brokerId !== this.brokerId) {
1021
1171
  const errorMessage = this.messages.errors.signInDidNotReturnSession;
1022
1172
  this.setState({ ...this.state, errorMessage, status: 'error' });
1023
1173
  this.callbacks.onError?.(errorMessage);
1024
- pending.resolve(false);
1174
+ this.resolvePendingLogin(pending.nonce, false, true);
1025
1175
  return;
1026
1176
  }
1027
- this.setSession({
1028
- sessionToken: message.sessionToken,
1029
- expiresAt: message.expiresAt,
1030
- });
1031
- try {
1032
- pending.popup?.close();
1033
- }
1034
- catch { }
1035
- this.reloadBridge();
1036
- pending.resolve(true);
1177
+ this.pingBridge();
1178
+ this.resolvePendingLogin(pending.nonce, true, true);
1037
1179
  return;
1038
1180
  }
1039
1181
  const errorMessage = message.message || this.messages.errors.signInFailed;
1040
1182
  this.setState({ ...this.state, errorMessage, status: 'error' });
1041
1183
  this.callbacks.onError?.(errorMessage);
1042
- pending.resolve(false);
1184
+ this.resolvePendingLogin(pending.nonce, false, true);
1043
1185
  }
1044
1186
  handleEditorMessage(message) {
1045
1187
  if (this.navigatorNonce && message.nonce === this.navigatorNonce) {
@@ -1133,24 +1275,13 @@ class EinblickEditRuntime {
1133
1275
  }
1134
1276
  }
1135
1277
  setSession(session) {
1136
- const previousSessionToken = this.session?.sessionToken ?? null;
1278
+ const wasActive = this.session !== null;
1137
1279
  this.session = session && !isEditSessionExpired(session) ? session : null;
1138
- const nextSessionToken = this.session?.sessionToken ?? null;
1139
- if (previousSessionToken !== nextSessionToken) {
1280
+ if (wasActive !== (this.session !== null)) {
1140
1281
  this.removeNavigatorPanel(false);
1141
1282
  }
1142
1283
  this.callbacks.onSessionChange(this.session);
1143
1284
  }
1144
- getActiveSessionToken() {
1145
- if (!this.session) {
1146
- return null;
1147
- }
1148
- if (isEditSessionExpired(this.session)) {
1149
- this.setSession(null);
1150
- return null;
1151
- }
1152
- return this.session.sessionToken;
1153
- }
1154
1285
  setState(nextState) {
1155
1286
  this.state = nextState;
1156
1287
  this.callbacks.onStateChange(nextState);
@@ -1739,10 +1870,15 @@ class EinblickEditRuntime {
1739
1870
  }
1740
1871
  }
1741
1872
  async logoutFromEinblick() {
1873
+ await this.endEditSession(true);
1874
+ }
1875
+ async deactivateEditSession() {
1876
+ await this.endEditSession(false);
1877
+ }
1878
+ async endEditSession(signOut) {
1742
1879
  if (typeof window === 'undefined') {
1743
1880
  return;
1744
1881
  }
1745
- const sessionToken = this.getActiveSessionToken();
1746
1882
  this.setSession(null);
1747
1883
  this.setState({
1748
1884
  enabled: true,
@@ -1752,13 +1888,13 @@ class EinblickEditRuntime {
1752
1888
  siteName: this.state.siteName,
1753
1889
  errorMessage: undefined,
1754
1890
  });
1755
- const loggedOutViaBridge = await this.requestBridgeLogout();
1891
+ const loggedOutViaBridge = await this.requestBridgeLogout(signOut);
1756
1892
  if (!loggedOutViaBridge) {
1757
- await this.logoutViaPopup(sessionToken);
1893
+ await this.logoutViaPopup(signOut);
1758
1894
  }
1759
1895
  this.reloadBridge();
1760
1896
  }
1761
- requestBridgeLogout() {
1897
+ requestBridgeLogout(signOut) {
1762
1898
  if (typeof window === 'undefined' || this.pendingLogout) {
1763
1899
  return Promise.resolve(false);
1764
1900
  }
@@ -1782,6 +1918,7 @@ class EinblickEditRuntime {
1782
1918
  type: 'logout',
1783
1919
  nonce: this.bridgeNonce,
1784
1920
  requestId,
1921
+ signOut,
1785
1922
  }, this.appOrigin);
1786
1923
  }
1787
1924
  catch {
@@ -1793,12 +1930,12 @@ class EinblickEditRuntime {
1793
1930
  }
1794
1931
  });
1795
1932
  }
1796
- async logoutViaPopup(sessionToken) {
1933
+ async logoutViaPopup(signOut) {
1797
1934
  const logoutUrl = this.buildBridgeUrl({
1798
1935
  mode: 'logout',
1799
1936
  nonce: createRandomId(),
1800
1937
  returnTo: window.location.href,
1801
- sessionToken,
1938
+ signOut,
1802
1939
  });
1803
1940
  let popup = null;
1804
1941
  try {
@@ -2046,23 +2183,22 @@ class EinblickEditRuntime {
2046
2183
  frame.src = this.buildBridgeUrl({ mode: 'bridge', nonce: this.bridgeNonce });
2047
2184
  this.bridgeFrame = frame;
2048
2185
  document.body.append(frame);
2049
- const sendPing = () => {
2050
- if (!this.bridgeFrame?.contentWindow) {
2051
- return;
2052
- }
2053
- try {
2054
- this.bridgeFrame.contentWindow.postMessage({
2055
- source: EINBLICK_PARENT_SOURCE,
2056
- type: 'bridge-ping',
2057
- nonce: this.bridgeNonce,
2058
- }, this.appOrigin);
2059
- }
2060
- catch { }
2061
- };
2062
2186
  frame.addEventListener('load', () => {
2063
- sendPing();
2187
+ this.pingBridge();
2064
2188
  });
2065
- this.bridgePingInterval = window.setInterval(sendPing, 1500);
2189
+ this.bridgePingInterval = window.setInterval(() => this.pingBridge(), 1500);
2190
+ }
2191
+ pingBridge() {
2192
+ if (!this.bridgeFrame?.contentWindow)
2193
+ return;
2194
+ try {
2195
+ this.bridgeFrame.contentWindow.postMessage({
2196
+ source: EINBLICK_PARENT_SOURCE,
2197
+ type: 'bridge-ping',
2198
+ nonce: this.bridgeNonce,
2199
+ }, this.appOrigin);
2200
+ }
2201
+ catch { }
2066
2202
  }
2067
2203
  clearBridgePing() {
2068
2204
  if (this.bridgePingInterval !== null) {
@@ -2075,28 +2211,33 @@ class EinblickEditRuntime {
2075
2211
  url.searchParams.set('siteKey', this.siteKey);
2076
2212
  url.searchParams.set('origin', window.location.origin);
2077
2213
  url.searchParams.set('nonce', args.nonce);
2214
+ url.searchParams.set('protocol', '2');
2215
+ url.searchParams.set('persist', this.persist ? '1' : '0');
2078
2216
  if (args.mode !== 'bridge') {
2079
2217
  url.searchParams.set('mode', args.mode);
2080
2218
  }
2081
- const sessionToken = args.sessionToken ??
2082
- (args.mode === 'bridge' ? this.getActiveSessionToken() : null);
2083
- if (sessionToken) {
2084
- url.searchParams.set('sessionToken', sessionToken);
2219
+ const brokerId = args.brokerId ?? (args.mode === 'login' ? this.brokerId : null);
2220
+ if (brokerId) {
2221
+ url.searchParams.set('brokerId', brokerId);
2085
2222
  }
2086
2223
  if (args.returnTo) {
2087
2224
  url.searchParams.set('returnTo', args.returnTo);
2088
2225
  }
2226
+ if (args.signOut === false) {
2227
+ url.searchParams.set('signOut', '0');
2228
+ }
2089
2229
  return url.toString();
2090
2230
  }
2091
2231
  buildEditorUrl(binding, nonce, intent = 'edit') {
2092
- const sessionToken = this.getActiveSessionToken();
2093
- if (!sessionToken) {
2232
+ if (!this.session || isEditSessionExpired(this.session)) {
2094
2233
  throw new Error(this.messages.errors.editSessionExpired);
2095
2234
  }
2096
2235
  const url = new URL('/sdk/editor', this.appOrigin);
2097
2236
  url.searchParams.set('origin', window.location.origin);
2098
2237
  url.searchParams.set('nonce', nonce);
2099
- url.searchParams.set('sessionToken', sessionToken);
2238
+ url.searchParams.set('siteKey', this.siteKey);
2239
+ url.searchParams.set('protocol', '2');
2240
+ url.searchParams.set('persist', this.persist ? '1' : '0');
2100
2241
  url.searchParams.set('returnTo', window.location.href);
2101
2242
  url.searchParams.set('sourceType', binding.sourceType ?? 'cms');
2102
2243
  url.searchParams.set('resourceSlug', binding.resourceSlug);
@@ -2115,25 +2256,27 @@ class EinblickEditRuntime {
2115
2256
  return url.toString();
2116
2257
  }
2117
2258
  buildNavigatorUrl(nonce) {
2118
- const sessionToken = this.getActiveSessionToken();
2119
- if (!sessionToken) {
2259
+ if (!this.session || isEditSessionExpired(this.session)) {
2120
2260
  throw new Error(this.messages.errors.editSessionExpired);
2121
2261
  }
2122
2262
  const url = new URL('/sdk/navigator', this.appOrigin);
2123
2263
  url.searchParams.set('origin', window.location.origin);
2124
2264
  url.searchParams.set('nonce', nonce);
2125
- url.searchParams.set('sessionToken', sessionToken);
2265
+ url.searchParams.set('siteKey', this.siteKey);
2266
+ url.searchParams.set('protocol', '2');
2267
+ url.searchParams.set('persist', this.persist ? '1' : '0');
2126
2268
  return url.toString();
2127
2269
  }
2128
2270
  buildCollectionEditorUrl(args) {
2129
- const sessionToken = this.getActiveSessionToken();
2130
- if (!sessionToken) {
2271
+ if (!this.session || isEditSessionExpired(this.session)) {
2131
2272
  throw new Error(this.messages.errors.editSessionExpired);
2132
2273
  }
2133
2274
  const url = new URL('/sdk/editor', this.appOrigin);
2134
2275
  url.searchParams.set('origin', window.location.origin);
2135
2276
  url.searchParams.set('nonce', args.nonce);
2136
- url.searchParams.set('sessionToken', sessionToken);
2277
+ url.searchParams.set('siteKey', this.siteKey);
2278
+ url.searchParams.set('protocol', '2');
2279
+ url.searchParams.set('persist', this.persist ? '1' : '0');
2137
2280
  url.searchParams.set('returnTo', window.location.href);
2138
2281
  url.searchParams.set('sourceType', args.sourceType ?? 'cms');
2139
2282
  url.searchParams.set('collectionId', args.collectionId);
@@ -2175,6 +2318,62 @@ class EinblickEditRuntime {
2175
2318
  if (typeof document === 'undefined') {
2176
2319
  return;
2177
2320
  }
2321
+ for (const [template, control] of this.collectionCreateControls) {
2322
+ if (!template.isConnected || !control.container.isConnected) {
2323
+ control.container.remove();
2324
+ this.collectionCreateControls.delete(template);
2325
+ }
2326
+ }
2327
+ const templates = Array.from(document.querySelectorAll('template[data-einblick-collection-create-template="true"]'));
2328
+ for (const template of templates) {
2329
+ const collection = template.closest('[data-einblick-editable="true"][data-einblick-kind="collection"]');
2330
+ const binding = collection ? readBindingFromElement(collection) : null;
2331
+ const isVisible = !!collection &&
2332
+ !!binding &&
2333
+ this.shouldShowCreateButton(collection, binding);
2334
+ const existing = this.collectionCreateControls.get(template);
2335
+ if (!isVisible || !collection) {
2336
+ if (existing) {
2337
+ existing.container.remove();
2338
+ this.collectionCreateControls.delete(template);
2339
+ }
2340
+ continue;
2341
+ }
2342
+ let control = existing;
2343
+ if (!control) {
2344
+ const button = document.createElement('button');
2345
+ button.type = 'button';
2346
+ button.setAttribute('data-einblick-collection-create-button', 'true');
2347
+ button.setAttribute('data-einblick-runtime-created', 'true');
2348
+ const container = collection.tagName === 'UL' || collection.tagName === 'OL'
2349
+ ? document.createElement('li')
2350
+ : button;
2351
+ if (container !== button) {
2352
+ container.setAttribute('data-einblick-collection-create-wrapper', 'true');
2353
+ container.append(button);
2354
+ }
2355
+ template.insertAdjacentElement('afterend', container);
2356
+ control = { button, container };
2357
+ this.collectionCreateControls.set(template, control);
2358
+ }
2359
+ const buttonClass = template.dataset.einblickCollectionCreateButtonClass ?? '';
2360
+ if (control.button.className !== buttonClass) {
2361
+ control.button.className = buttonClass;
2362
+ }
2363
+ const explicitLabel = template.dataset.einblickCollectionCreateLabel?.trim();
2364
+ if (explicitLabel) {
2365
+ control.button.dataset.einblickCollectionCreateLabel = explicitLabel;
2366
+ }
2367
+ else {
2368
+ delete control.button.dataset.einblickCollectionCreateLabel;
2369
+ }
2370
+ if (control.container !== control.button) {
2371
+ const wrapperClass = template.dataset.einblickCollectionCreateWrapperClass ?? '';
2372
+ if (control.container.className !== wrapperClass) {
2373
+ control.container.className = wrapperClass;
2374
+ }
2375
+ }
2376
+ }
2178
2377
  const buttons = Array.from(document.querySelectorAll('[data-einblick-collection-create-button="true"]'));
2179
2378
  for (const button of buttons) {
2180
2379
  const label = button.dataset.einblickCollectionCreateLabel?.trim() ||
@@ -2188,6 +2387,13 @@ class EinblickEditRuntime {
2188
2387
  this.shouldShowCreateButton(collection, binding);
2189
2388
  button.hidden = !isVisible;
2190
2389
  button.disabled = !isVisible;
2390
+ button.tabIndex = isVisible ? 0 : -1;
2391
+ if (isVisible) {
2392
+ button.removeAttribute('aria-hidden');
2393
+ }
2394
+ else {
2395
+ button.setAttribute('aria-hidden', 'true');
2396
+ }
2191
2397
  const wrapper = button.closest('[data-einblick-collection-create-wrapper="true"]');
2192
2398
  if (wrapper) {
2193
2399
  wrapper.hidden = !isVisible;
@@ -2195,7 +2401,8 @@ class EinblickEditRuntime {
2195
2401
  }
2196
2402
  }
2197
2403
  shouldShowEditButton(element, binding) {
2198
- if (element?.dataset.einblickKind === 'collection') {
2404
+ if (!this.state.isAuthenticated ||
2405
+ element?.dataset.einblickKind === 'collection') {
2199
2406
  return false;
2200
2407
  }
2201
2408
  return isRecordEditableBinding(binding);
@@ -2718,6 +2925,7 @@ class EinblickEditRuntime {
2718
2925
  });
2719
2926
  document.body.append(backdrop);
2720
2927
  this.editorBackdrop = backdrop;
2928
+ this.editorFrame = iframe;
2721
2929
  }
2722
2930
  openCollectionDrawer(args) {
2723
2931
  if (!this.state.isAuthenticated) {
@@ -2774,6 +2982,7 @@ class EinblickEditRuntime {
2774
2982
  });
2775
2983
  document.body.append(backdrop);
2776
2984
  this.editorBackdrop = backdrop;
2985
+ this.editorFrame = iframe;
2777
2986
  }
2778
2987
  isBottomBarMenuOpen() {
2779
2988
  return !!this.bottomBarMoreMenu && !this.bottomBarMoreMenu.hidden;
@@ -2789,6 +2998,7 @@ class EinblickEditRuntime {
2789
2998
  closeDrawer() {
2790
2999
  this.editorBackdrop?.remove();
2791
3000
  this.editorBackdrop = null;
3001
+ this.editorFrame = null;
2792
3002
  this.editorNonce = null;
2793
3003
  this.editorTarget = null;
2794
3004
  this.editorBinding = null;
@@ -2820,6 +3030,8 @@ export function createEinblickEditRuntime(options) {
2820
3030
  start: () => runtime.start(),
2821
3031
  destroy: () => runtime.destroy(),
2822
3032
  login: (loginOptions) => runtime.login(loginOptions),
3033
+ deactivate: () => runtime.deactivateEditSession(),
3034
+ logout: () => runtime.logoutFromEinblick(),
2823
3035
  openEditor: (binding, element) => runtime.openEditor(binding, element),
2824
3036
  setLocale: (locale) => runtime.setLocale(locale),
2825
3037
  setAccentColor: (accentColor) => runtime.setAccentColor(accentColor),