@sparkvault/sdk 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5098,8 +5098,8 @@ class IdentityRenderer {
5098
5098
  * Implements the Container interface for use with IdentityRenderer.
5099
5099
  */
5100
5100
  const DEFAULT_OPTIONS$1 = {
5101
- showHeader: true,
5102
- showCloseButton: true,
5101
+ showHeader: false,
5102
+ showCloseButton: false,
5103
5103
  showFooter: true,
5104
5104
  };
5105
5105
  class InlineContainer {
@@ -5290,6 +5290,36 @@ function onDomReady(callback) {
5290
5290
  callback();
5291
5291
  }
5292
5292
  }
5293
+ /**
5294
+ * Execute a callback when document.body is available.
5295
+ * Handles edge cases where body might not exist even after DOMContentLoaded
5296
+ * (e.g., scripts with defer/async loading during parsing).
5297
+ */
5298
+ function onBodyReady(callback) {
5299
+ if (document.body) {
5300
+ callback();
5301
+ }
5302
+ else {
5303
+ // Body not yet available - wait for DOMContentLoaded
5304
+ // or poll briefly as a fallback for edge cases
5305
+ const checkBody = () => {
5306
+ if (document.body) {
5307
+ callback();
5308
+ }
5309
+ else {
5310
+ // Rare edge case: poll for body availability
5311
+ window.requestAnimationFrame(checkBody);
5312
+ }
5313
+ };
5314
+ if (document.readyState === 'loading') {
5315
+ document.addEventListener('DOMContentLoaded', checkBody, { once: true });
5316
+ }
5317
+ else {
5318
+ // Document ready but no body yet - poll
5319
+ window.requestAnimationFrame(checkBody);
5320
+ }
5321
+ }
5322
+ }
5293
5323
 
5294
5324
  /* global Element */
5295
5325
  /**
@@ -5451,7 +5481,11 @@ class IdentityModule {
5451
5481
  });
5452
5482
  });
5453
5483
  });
5454
- observer.observe(document.body, { childList: true, subtree: true });
5484
+ // Observe document.body for dynamic elements
5485
+ // Use onBodyReady to handle edge cases where body isn't available yet
5486
+ onBodyReady(() => {
5487
+ observer?.observe(document.body, { childList: true, subtree: true });
5488
+ });
5455
5489
  };
5456
5490
  // Defer attachment until DOM is ready
5457
5491
  onDomReady(attachToElements);
@@ -5752,18 +5786,32 @@ function getUploadStyles(options) {
5752
5786
  ======================================== */
5753
5787
 
5754
5788
  .svu-overlay {
5755
- position: fixed;
5756
- inset: 0;
5789
+ /* Force full-screen overlay regardless of page styles */
5790
+ position: fixed !important;
5791
+ top: 0 !important;
5792
+ left: 0 !important;
5793
+ right: 0 !important;
5794
+ bottom: 0 !important;
5795
+ width: 100vw !important;
5796
+ height: 100vh !important;
5797
+ max-width: none !important;
5798
+ max-height: none !important;
5799
+ margin: 0 !important;
5757
5800
  background: rgba(0, 0, 0, 0.5);
5758
- display: flex;
5801
+ display: flex !important;
5759
5802
  align-items: center;
5760
5803
  justify-content: center;
5761
- z-index: 999999;
5804
+ z-index: 999999 !important;
5762
5805
  padding: 16px;
5763
5806
  box-sizing: border-box;
5764
5807
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
5765
5808
  -webkit-font-smoothing: antialiased;
5766
5809
  -moz-osx-font-smoothing: grayscale;
5810
+ /* Break out of any containing block that could affect fixed positioning */
5811
+ transform: none !important;
5812
+ filter: none !important;
5813
+ contain: none !important;
5814
+ isolation: isolate;
5767
5815
  }
5768
5816
 
5769
5817
  .svu-overlay.svu-blur {
@@ -6972,6 +7020,9 @@ class UploadModalContainer {
6972
7020
  setDarkMode(enabled) {
6973
7021
  if (!this.elements)
6974
7022
  return;
7023
+ // Skip if already in the requested mode
7024
+ if (this.isDarkMode === enabled)
7025
+ return;
6975
7026
  this.isDarkMode = enabled;
6976
7027
  if (enabled) {
6977
7028
  this.elements.modal.classList.add('svu-dark');
@@ -6979,7 +7030,7 @@ class UploadModalContainer {
6979
7030
  else {
6980
7031
  this.elements.modal.classList.remove('svu-dark');
6981
7032
  }
6982
- // Re-create header with correct logo
7033
+ // Re-create header with correct logo (only when mode actually changes)
6983
7034
  const newHeader = this.createHeader(this.branding);
6984
7035
  if (this.headerElement?.parentNode) {
6985
7036
  this.headerElement.parentNode.replaceChild(newHeader, this.headerElement);
@@ -7085,7 +7136,7 @@ class UploadModalContainer {
7085
7136
  <div class="svu-security-badges">
7086
7137
  <span class="svu-tls-badge">TLS 1.3 In Transit</span>
7087
7138
  <span class="svu-tls-badge">Encrypted At Rest</span>
7088
- <span class="svu-tls-badge">Zero Knowledge</span>
7139
+ <span class="svu-tls-badge">Zero Trust</span>
7089
7140
  </div>
7090
7141
  `;
7091
7142
  sidebar.appendChild(security);
@@ -7151,8 +7202,8 @@ class UploadModalContainer {
7151
7202
  * Implements the UploadContainer interface for use with UploadRenderer.
7152
7203
  */
7153
7204
  const DEFAULT_OPTIONS = {
7154
- showHeader: true,
7155
- showCloseButton: true,
7205
+ showHeader: false,
7206
+ showCloseButton: false,
7156
7207
  showFooter: true,
7157
7208
  };
7158
7209
  class UploadInlineContainer {
@@ -7246,6 +7297,9 @@ class UploadInlineContainer {
7246
7297
  setDarkMode(enabled) {
7247
7298
  if (!this.container)
7248
7299
  return;
7300
+ // Skip if already in the requested mode
7301
+ if (this.isDarkMode === enabled)
7302
+ return;
7249
7303
  this.isDarkMode = enabled;
7250
7304
  if (enabled) {
7251
7305
  this.container.classList.add('svu-dark');
@@ -7253,7 +7307,7 @@ class UploadInlineContainer {
7253
7307
  else {
7254
7308
  this.container.classList.remove('svu-dark');
7255
7309
  }
7256
- // Re-create header with correct logo
7310
+ // Re-create header with correct logo (only when mode actually changes)
7257
7311
  if (this.containerOptions.showHeader && this.header) {
7258
7312
  const newHeader = this.createHeader(this.branding);
7259
7313
  this.container.replaceChild(newHeader, this.header);
@@ -7441,7 +7495,7 @@ class UploadInlineContainer {
7441
7495
  <div class="svu-security-badges">
7442
7496
  <span class="svu-tls-badge">TLS 1.3 In Transit</span>
7443
7497
  <span class="svu-tls-badge">Encrypted At Rest</span>
7444
- <span class="svu-tls-badge">Zero Knowledge</span>
7498
+ <span class="svu-tls-badge">Zero Trust</span>
7445
7499
  </div>
7446
7500
  `;
7447
7501
  sidebar.appendChild(security);
@@ -7508,6 +7562,7 @@ class UploadRenderer {
7508
7562
  // Hybrid mode: modal overlay for upload/ceremony in inline mode
7509
7563
  this.uploadModal = null;
7510
7564
  this.isInlineMode = false;
7565
+ this.hybridModalInitialized = false;
7511
7566
  this.pasteHandler = null;
7512
7567
  this.container = container;
7513
7568
  this.api = api;
@@ -7548,6 +7603,7 @@ class UploadRenderer {
7548
7603
  if (this.uploadModal) {
7549
7604
  this.uploadModal.destroy();
7550
7605
  this.uploadModal = null;
7606
+ this.hybridModalInitialized = false;
7551
7607
  }
7552
7608
  this.container.destroy();
7553
7609
  }
@@ -7567,10 +7623,16 @@ class UploadRenderer {
7567
7623
  this.renderInModal();
7568
7624
  return;
7569
7625
  }
7570
- // Close modal if we're leaving upload/ceremony states
7626
+ // Close modal if we're leaving upload/ceremony states (hybrid mode complete)
7571
7627
  if (this.uploadModal) {
7572
7628
  this.uploadModal.destroy();
7573
7629
  this.uploadModal = null;
7630
+ this.hybridModalInitialized = false;
7631
+ // Restore inline container visibility
7632
+ const inlineBody = this.container.getBody();
7633
+ if (inlineBody?.parentElement) {
7634
+ inlineBody.parentElement.style.display = '';
7635
+ }
7574
7636
  }
7575
7637
  const body = this.container.getBody();
7576
7638
  if (!body)
@@ -7609,7 +7671,7 @@ class UploadRenderer {
7609
7671
  * This provides the full polished experience during upload.
7610
7672
  */
7611
7673
  renderInModal() {
7612
- // Create modal if not exists
7674
+ // Create and initialize modal only once
7613
7675
  if (!this.uploadModal) {
7614
7676
  this.uploadModal = new UploadModalContainer();
7615
7677
  this.uploadModal.createLoading({ backdropBlur: this.options.backdropBlur ?? true }, () => { } // No close callback - user can't cancel during upload
@@ -7617,15 +7679,25 @@ class UploadRenderer {
7617
7679
  if (this.config) {
7618
7680
  this.uploadModal.updateBranding(this.config.branding);
7619
7681
  }
7682
+ this.hybridModalInitialized = false;
7620
7683
  }
7621
7684
  const body = this.uploadModal.getBody();
7622
7685
  if (!body)
7623
7686
  return;
7687
+ // Initialize modal state only once (not on every progress update)
7688
+ if (!this.hybridModalInitialized) {
7689
+ // Hide inline container during hybrid mode to avoid CSS conflicts
7690
+ const inlineBody = this.container.getBody();
7691
+ if (inlineBody?.parentElement) {
7692
+ inlineBody.parentElement.style.display = 'none';
7693
+ }
7694
+ // Set dark mode and show sidebar once
7695
+ this.uploadModal.setDarkMode(true);
7696
+ this.uploadModal.toggleSidebar(true);
7697
+ this.hybridModalInitialized = true;
7698
+ }
7624
7699
  // Clear and render current state
7625
7700
  body.innerHTML = '';
7626
- // Set dark mode and show sidebar
7627
- this.uploadModal.setDarkMode(true);
7628
- this.uploadModal.toggleSidebar(true);
7629
7701
  if (this.viewState.view === 'uploading') {
7630
7702
  body.appendChild(this.renderUploading(this.viewState));
7631
7703
  }
@@ -8074,45 +8146,81 @@ class UploadRenderer {
8074
8146
  }
8075
8147
  }
8076
8148
  async uploadWithTus(file, forgeUrl, ingotId, requestId) {
8077
- // Simple XHR upload with progress (tus-like behavior)
8078
- // Timeout: 10 minutes for large file uploads
8079
- const UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
8080
- return new Promise((resolve, reject) => {
8081
- const xhr = new XMLHttpRequest();
8082
- xhr.timeout = UPLOAD_TIMEOUT_MS;
8083
- xhr.upload.onprogress = (e) => {
8084
- if (e.lengthComputable) {
8085
- const progress = Math.round((e.loaded / e.total) * 100);
8086
- this.setState({
8087
- view: 'uploading',
8088
- file,
8089
- ingotId,
8090
- requestId,
8091
- progress,
8092
- bytesUploaded: e.loaded,
8093
- });
8094
- this.callbacks.onProgress?.({
8095
- bytesUploaded: e.loaded,
8096
- bytesTotal: e.total,
8097
- percentage: progress,
8098
- phase: 'uploading',
8099
- });
8100
- }
8101
- };
8102
- xhr.onload = () => {
8103
- if (xhr.status >= 200 && xhr.status < 300) {
8104
- resolve();
8105
- }
8106
- else {
8107
- reject(new Error(`Upload failed with status ${xhr.status}`));
8108
- }
8109
- };
8110
- xhr.onerror = () => reject(new Error('Upload failed'));
8111
- xhr.ontimeout = () => reject(new Error('Upload timed out'));
8112
- xhr.open('POST', forgeUrl);
8113
- xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
8114
- xhr.send(file);
8149
+ // tus resumable upload protocol v1.0.0
8150
+ // Uploads file in chunks to avoid Cloudflare's 100MB request limit
8151
+ const TUS_VERSION = '1.0.0';
8152
+ const DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB default, server may override
8153
+ // Extract base URL and ISTK from forge URL
8154
+ const url = new URL(forgeUrl);
8155
+ const istk = url.searchParams.get('istk');
8156
+ const baseUrl = `${url.origin}${url.pathname}`;
8157
+ if (!istk) {
8158
+ throw new Error('Missing ISTK in forge URL');
8159
+ }
8160
+ // Step 1: Create tus upload session
8161
+ const createResponse = await fetch(baseUrl, {
8162
+ method: 'POST',
8163
+ headers: {
8164
+ 'Tus-Resumable': TUS_VERSION,
8165
+ 'Upload-Length': String(file.size),
8166
+ 'X-ISTK': istk,
8167
+ 'Content-Type': 'application/offset+octet-stream',
8168
+ },
8115
8169
  });
8170
+ if (!createResponse.ok) {
8171
+ const errorText = await createResponse.text();
8172
+ throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
8173
+ }
8174
+ // Get upload location and chunk size from response
8175
+ const location = createResponse.headers.get('Location');
8176
+ const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
8177
+ const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : DEFAULT_CHUNK_SIZE;
8178
+ if (!location) {
8179
+ throw new Error('Server did not return upload location');
8180
+ }
8181
+ // Build full upload URL
8182
+ const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
8183
+ // Step 2: Upload file in chunks
8184
+ let offset = 0;
8185
+ const totalSize = file.size;
8186
+ while (offset < totalSize) {
8187
+ const end = Math.min(offset + chunkSize, totalSize);
8188
+ const chunk = file.slice(offset, end);
8189
+ // Upload chunk
8190
+ const patchResponse = await fetch(uploadUrl, {
8191
+ method: 'PATCH',
8192
+ headers: {
8193
+ 'Tus-Resumable': TUS_VERSION,
8194
+ 'Upload-Offset': String(offset),
8195
+ 'Content-Type': 'application/offset+octet-stream',
8196
+ },
8197
+ body: chunk,
8198
+ });
8199
+ if (!patchResponse.ok) {
8200
+ const errorText = await patchResponse.text();
8201
+ throw new Error(`Chunk upload failed: ${patchResponse.status} ${errorText}`);
8202
+ }
8203
+ // Update offset from server response
8204
+ const newOffsetHeader = patchResponse.headers.get('Upload-Offset');
8205
+ const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : end;
8206
+ offset = newOffset;
8207
+ // Update progress
8208
+ const progress = Math.round((offset / totalSize) * 100);
8209
+ this.setState({
8210
+ view: 'uploading',
8211
+ file,
8212
+ ingotId,
8213
+ requestId,
8214
+ progress,
8215
+ bytesUploaded: offset,
8216
+ });
8217
+ this.callbacks.onProgress?.({
8218
+ bytesUploaded: offset,
8219
+ bytesTotal: totalSize,
8220
+ percentage: progress,
8221
+ phase: 'uploading',
8222
+ });
8223
+ }
8116
8224
  }
8117
8225
  async runCeremony(file, ingotId, requestId) {
8118
8226
  for (let i = 0; i < CEREMONY_STEPS.length; i++) {