@proveanything/smartlinks 2.0.40 → 2.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -371,6 +371,8 @@ await manager.loadApp('app-container');
371
371
 
372
372
  ## Message Protocol
373
373
 
374
+ > **Every proxy request needs an answer.** An app in `proxyMode` sends its API calls to this page by `postMessage` and waits for the reply. Since 2.0.41 it gives up after `proxyTimeoutMs` (default 90 s; `initializeApi({ proxyTimeoutMs })`) with a 504 `SmartlinksApiError` (`errorCode: 'PROXY_TIMEOUT'`) — before that, a host that didn't answer left the call hanging with no network request and no error. A host must reply to every `_smartlinksProxyRequest`, with an `error` if it can't serve it.
375
+
374
376
  The responder handles these message types from the iframe:
375
377
 
376
378
  ### Route Changes
@@ -468,311 +470,311 @@ import type {
468
470
  ### Portal back exits the app too early
469
471
  - Set `state.parentPath` on route changes for screens that should navigate "up" inside the app.
470
472
  - Make sure the receiving app listens for `smartlinks-navigate` if the SDK version in use does not already handle it.
471
-
472
- ---
473
-
474
- ## Streaming in a hand-rolled parent (without IframeResponder)
475
-
476
- The following applies **only if you build your own parent proxy** instead of using the `IframeResponder` class above — which already handles AI streaming for you. It is the wire protocol for forwarding SSE/streaming responses to a child app in proxy mode.
477
- ### Goal
478
-
479
- Keep the existing architecture:
480
-
481
- - local mode: child calls API directly
482
- - iframe proxy mode: child never owns auth state and streams through the parent
483
-
484
- This keeps user/session authority in the parent while making AI streaming behave like the rest of the SDK transport.
485
-
486
- ### What changed
487
-
488
- Previously, proxy mode only supported one-shot request/response messages:
489
-
490
- - `_smartlinksProxyRequest`
491
- - `_smartlinksProxyResponse`
492
-
493
- Streaming now adds a second protocol for long-lived responses:
494
-
495
- - `_smartlinksProxyStreamRequest`
496
- - `_smartlinksProxyStream`
497
- - `_smartlinksProxyStreamAbort`
498
-
499
- ### New parent message handling
500
-
501
- #### 1. Listen for stream requests
502
-
503
- The iframe child may now send this message:
504
-
505
- ```ts
506
- {
507
- _smartlinksProxyStreamRequest: true,
508
- id: string,
509
- method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
510
- path: string,
511
- body?: any,
512
- headers?: Record<string, string>
513
- }
514
- ```
515
-
516
- Parent behavior:
517
-
518
- - treat this like a proxied API request
519
- - build the real API URL from your configured base URL plus `path`
520
- - send the request using the parent's current auth/session context
521
- - expect an SSE / streaming response body
522
- - keep the request open until the stream ends or is aborted
523
-
524
- #### 2. Forward stream lifecycle messages back to the child
525
-
526
- The parent should send messages back to the iframe using this envelope:
527
-
528
- ```ts
529
- {
530
- _smartlinksProxyStream: true,
531
- id: string,
532
- phase: 'open' | 'event' | 'end' | 'error',
533
- data?: any,
534
- error?: string,
535
- status?: number
536
- }
537
- ```
538
-
539
- Phases:
540
-
541
- - `open`
542
- - optional but recommended
543
- - indicates the upstream streaming request was accepted and a body exists
544
- - `event`
545
- - contains one parsed JSON event from an SSE `data:` frame
546
- - send one message per logical event payload
547
- - `end`
548
- - sent once when the stream finishes normally
549
- - `error`
550
- - sent if the upstream request fails before or during streaming
551
-
552
- #### 3. Support abort from the child
553
-
554
- The child may stop reading early and send:
555
-
556
- ```ts
557
- {
558
- _smartlinksProxyStreamAbort: true,
559
- id: string
560
- }
561
- ```
562
-
563
- Parent behavior:
564
-
565
- - look up the active stream by `id`
566
- - abort the underlying fetch / reader
567
- - clean up any local state for that stream
568
- - do not keep streaming after abort
569
-
570
- ### SSE forwarding rules
571
-
572
- The upstream AI endpoints return SSE-like frames. The parent should:
573
-
574
- - read the response body as a stream
575
- - buffer text until line boundaries
576
- - collect `data:` lines for a single event
577
- - join multi-line `data:` payloads with `\n`
578
- - ignore blank events
579
- - stop on `data: [DONE]`
580
- - JSON-parse each event payload
581
- - forward parsed payloads to the iframe as `_smartlinksProxyStream` with `phase: 'event'`
582
-
583
- Minimal parsing behavior:
584
-
585
- 1. accumulate bytes into text
586
- 2. split on `\r?\n`
587
- 3. collect each `data:` line
588
- 4. on blank line, finalize the event
589
- 5. if payload is `[DONE]`, finish
590
- 6. otherwise `JSON.parse(payload)` and forward
591
-
592
- ### Auth and session expectations
593
-
594
- The parent remains the source of truth for auth.
595
-
596
- That means the parent stream handler should:
597
-
598
- - use the same auth headers/token source as normal proxied requests
599
- - not require the iframe to know the bearer token or API key
600
- - naturally pick up the current logged-in user when the stream starts
601
- - cancel active streams if your app invalidates session state on logout or account switch
602
-
603
- In practice, the stream request should use the same header-building logic as your normal parent proxy transport.
604
-
605
- ### Error handling expectations
606
-
607
- If the upstream fetch returns a non-2xx status:
608
-
609
- - try to read the JSON error body
610
- - derive a useful message
611
- - send one `_smartlinksProxyStream` message with `phase: 'error'`
612
- - include `status` when available
613
- - do not send `end` afterward
614
-
615
- If the stream body is missing unexpectedly:
616
-
617
- - send `phase: 'error'`
618
-
619
- If JSON parsing fails for a single event chunk:
620
-
621
- - safest behavior is to ignore that malformed chunk and continue
622
-
623
- ### State the parent should keep
624
-
625
- Track active streams in a map keyed by `id`:
626
-
627
- ```ts
628
- Map<string, AbortController>
629
- ```
630
-
631
- Recommended cleanup points:
632
-
633
- - on normal stream end
634
- - on error
635
- - on child abort
636
- - on iframe detach/unmount
637
- - on parent auth reset/logout if you want all in-flight streams cancelled immediately
638
-
639
- ### Parent implementation outline
640
-
641
- ```ts
642
- const activeStreams = new Map<string, AbortController>()
643
-
644
- window.addEventListener('message', async (event) => {
645
- const msg = event.data
646
-
647
- if (msg?._smartlinksProxyStreamAbort && msg.id) {
648
- activeStreams.get(msg.id)?.abort()
649
- activeStreams.delete(msg.id)
650
- return
651
- }
652
-
653
- if (msg?._smartlinksProxyStreamRequest && msg.id) {
654
- const controller = new AbortController()
655
- activeStreams.set(msg.id, controller)
656
-
657
- try {
658
- const response = await fetch(buildUrl(msg.path), {
659
- method: msg.method,
660
- headers: msg.headers,
661
- body: msg.body ? JSON.stringify(msg.body) : undefined,
662
- signal: controller.signal,
663
- })
664
-
665
- if (!response.ok || !response.body) {
666
- postError(...)
667
- return
668
- }
669
-
670
- postOpen(...)
671
- await forwardSse(response.body, parsed => postEvent(...parsed))
672
- postEnd(...)
673
- } catch (err) {
674
- if (err?.name !== 'AbortError') postError(...)
675
- } finally {
676
- activeStreams.delete(msg.id)
677
- }
678
- }
679
- })
680
- ```
681
-
682
- ### Exact protocol summary
683
-
684
- #### Child → parent
685
-
686
- Standard stream request:
687
-
688
- ```ts
689
- {
690
- _smartlinksProxyStreamRequest: true,
691
- id,
692
- method,
693
- path,
694
- body,
695
- headers
696
- }
697
- ```
698
-
699
- Abort request:
700
-
701
- ```ts
702
- {
703
- _smartlinksProxyStreamAbort: true,
704
- id
705
- }
706
- ```
707
-
708
- #### Parent → child
709
-
710
- Open:
711
-
712
- ```ts
713
- {
714
- _smartlinksProxyStream: true,
715
- id,
716
- phase: 'open'
717
- }
718
- ```
719
-
720
- Event:
721
-
722
- ```ts
723
- {
724
- _smartlinksProxyStream: true,
725
- id,
726
- phase: 'event',
727
- data: parsedJsonEvent
728
- }
729
- ```
730
-
731
- End:
732
-
733
- ```ts
734
- {
735
- _smartlinksProxyStream: true,
736
- id,
737
- phase: 'end'
738
- }
739
- ```
740
-
741
- Error:
742
-
743
- ```ts
744
- {
745
- _smartlinksProxyStream: true,
746
- id,
747
- phase: 'error',
748
- error: 'message',
749
- status?: number
750
- }
751
- ```
752
-
753
- ### What does not change
754
-
755
- These parts of the parent iframe integration stay the same:
756
-
757
- - normal `_smartlinksProxyRequest` request/response flow
758
- - upload proxy flow
759
- - auth login/logout postMessage handling
760
- - route/deep-link handling
761
- - resize handling
762
-
763
- This is an additive protocol, not a replacement.
764
-
765
- ### Current SDK reference
766
-
767
- The SDK implementation lives in:
768
-
769
- - [src/http.ts](src/http.ts)
770
- - [src/iframeResponder.ts](src/iframeResponder.ts)
771
- - [src/types/iframeResponder.ts](src/types/iframeResponder.ts)
772
- - [src/api/ai.ts](src/api/ai.ts)
773
-
774
- ### Practical recommendation
775
-
776
- If your parent already uses `IframeResponder`, prefer upgrading to the SDK version with these changes instead of re-implementing the protocol manually.
777
-
778
- If your parent has a custom iframe bridge, implement exactly the three new message types above and reuse your existing auth/header logic from normal proxied requests.
473
+
474
+ ---
475
+
476
+ ## Streaming in a hand-rolled parent (without IframeResponder)
477
+
478
+ The following applies **only if you build your own parent proxy** instead of using the `IframeResponder` class above — which already handles AI streaming for you. It is the wire protocol for forwarding SSE/streaming responses to a child app in proxy mode.
479
+ ### Goal
480
+
481
+ Keep the existing architecture:
482
+
483
+ - local mode: child calls API directly
484
+ - iframe proxy mode: child never owns auth state and streams through the parent
485
+
486
+ This keeps user/session authority in the parent while making AI streaming behave like the rest of the SDK transport.
487
+
488
+ ### What changed
489
+
490
+ Previously, proxy mode only supported one-shot request/response messages:
491
+
492
+ - `_smartlinksProxyRequest`
493
+ - `_smartlinksProxyResponse`
494
+
495
+ Streaming now adds a second protocol for long-lived responses:
496
+
497
+ - `_smartlinksProxyStreamRequest`
498
+ - `_smartlinksProxyStream`
499
+ - `_smartlinksProxyStreamAbort`
500
+
501
+ ### New parent message handling
502
+
503
+ #### 1. Listen for stream requests
504
+
505
+ The iframe child may now send this message:
506
+
507
+ ```ts
508
+ {
509
+ _smartlinksProxyStreamRequest: true,
510
+ id: string,
511
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
512
+ path: string,
513
+ body?: any,
514
+ headers?: Record<string, string>
515
+ }
516
+ ```
517
+
518
+ Parent behavior:
519
+
520
+ - treat this like a proxied API request
521
+ - build the real API URL from your configured base URL plus `path`
522
+ - send the request using the parent's current auth/session context
523
+ - expect an SSE / streaming response body
524
+ - keep the request open until the stream ends or is aborted
525
+
526
+ #### 2. Forward stream lifecycle messages back to the child
527
+
528
+ The parent should send messages back to the iframe using this envelope:
529
+
530
+ ```ts
531
+ {
532
+ _smartlinksProxyStream: true,
533
+ id: string,
534
+ phase: 'open' | 'event' | 'end' | 'error',
535
+ data?: any,
536
+ error?: string,
537
+ status?: number
538
+ }
539
+ ```
540
+
541
+ Phases:
542
+
543
+ - `open`
544
+ - optional but recommended
545
+ - indicates the upstream streaming request was accepted and a body exists
546
+ - `event`
547
+ - contains one parsed JSON event from an SSE `data:` frame
548
+ - send one message per logical event payload
549
+ - `end`
550
+ - sent once when the stream finishes normally
551
+ - `error`
552
+ - sent if the upstream request fails before or during streaming
553
+
554
+ #### 3. Support abort from the child
555
+
556
+ The child may stop reading early and send:
557
+
558
+ ```ts
559
+ {
560
+ _smartlinksProxyStreamAbort: true,
561
+ id: string
562
+ }
563
+ ```
564
+
565
+ Parent behavior:
566
+
567
+ - look up the active stream by `id`
568
+ - abort the underlying fetch / reader
569
+ - clean up any local state for that stream
570
+ - do not keep streaming after abort
571
+
572
+ ### SSE forwarding rules
573
+
574
+ The upstream AI endpoints return SSE-like frames. The parent should:
575
+
576
+ - read the response body as a stream
577
+ - buffer text until line boundaries
578
+ - collect `data:` lines for a single event
579
+ - join multi-line `data:` payloads with `\n`
580
+ - ignore blank events
581
+ - stop on `data: [DONE]`
582
+ - JSON-parse each event payload
583
+ - forward parsed payloads to the iframe as `_smartlinksProxyStream` with `phase: 'event'`
584
+
585
+ Minimal parsing behavior:
586
+
587
+ 1. accumulate bytes into text
588
+ 2. split on `\r?\n`
589
+ 3. collect each `data:` line
590
+ 4. on blank line, finalize the event
591
+ 5. if payload is `[DONE]`, finish
592
+ 6. otherwise `JSON.parse(payload)` and forward
593
+
594
+ ### Auth and session expectations
595
+
596
+ The parent remains the source of truth for auth.
597
+
598
+ That means the parent stream handler should:
599
+
600
+ - use the same auth headers/token source as normal proxied requests
601
+ - not require the iframe to know the bearer token or API key
602
+ - naturally pick up the current logged-in user when the stream starts
603
+ - cancel active streams if your app invalidates session state on logout or account switch
604
+
605
+ In practice, the stream request should use the same header-building logic as your normal parent proxy transport.
606
+
607
+ ### Error handling expectations
608
+
609
+ If the upstream fetch returns a non-2xx status:
610
+
611
+ - try to read the JSON error body
612
+ - derive a useful message
613
+ - send one `_smartlinksProxyStream` message with `phase: 'error'`
614
+ - include `status` when available
615
+ - do not send `end` afterward
616
+
617
+ If the stream body is missing unexpectedly:
618
+
619
+ - send `phase: 'error'`
620
+
621
+ If JSON parsing fails for a single event chunk:
622
+
623
+ - safest behavior is to ignore that malformed chunk and continue
624
+
625
+ ### State the parent should keep
626
+
627
+ Track active streams in a map keyed by `id`:
628
+
629
+ ```ts
630
+ Map<string, AbortController>
631
+ ```
632
+
633
+ Recommended cleanup points:
634
+
635
+ - on normal stream end
636
+ - on error
637
+ - on child abort
638
+ - on iframe detach/unmount
639
+ - on parent auth reset/logout if you want all in-flight streams cancelled immediately
640
+
641
+ ### Parent implementation outline
642
+
643
+ ```ts
644
+ const activeStreams = new Map<string, AbortController>()
645
+
646
+ window.addEventListener('message', async (event) => {
647
+ const msg = event.data
648
+
649
+ if (msg?._smartlinksProxyStreamAbort && msg.id) {
650
+ activeStreams.get(msg.id)?.abort()
651
+ activeStreams.delete(msg.id)
652
+ return
653
+ }
654
+
655
+ if (msg?._smartlinksProxyStreamRequest && msg.id) {
656
+ const controller = new AbortController()
657
+ activeStreams.set(msg.id, controller)
658
+
659
+ try {
660
+ const response = await fetch(buildUrl(msg.path), {
661
+ method: msg.method,
662
+ headers: msg.headers,
663
+ body: msg.body ? JSON.stringify(msg.body) : undefined,
664
+ signal: controller.signal,
665
+ })
666
+
667
+ if (!response.ok || !response.body) {
668
+ postError(...)
669
+ return
670
+ }
671
+
672
+ postOpen(...)
673
+ await forwardSse(response.body, parsed => postEvent(...parsed))
674
+ postEnd(...)
675
+ } catch (err) {
676
+ if (err?.name !== 'AbortError') postError(...)
677
+ } finally {
678
+ activeStreams.delete(msg.id)
679
+ }
680
+ }
681
+ })
682
+ ```
683
+
684
+ ### Exact protocol summary
685
+
686
+ #### Child → parent
687
+
688
+ Standard stream request:
689
+
690
+ ```ts
691
+ {
692
+ _smartlinksProxyStreamRequest: true,
693
+ id,
694
+ method,
695
+ path,
696
+ body,
697
+ headers
698
+ }
699
+ ```
700
+
701
+ Abort request:
702
+
703
+ ```ts
704
+ {
705
+ _smartlinksProxyStreamAbort: true,
706
+ id
707
+ }
708
+ ```
709
+
710
+ #### Parent → child
711
+
712
+ Open:
713
+
714
+ ```ts
715
+ {
716
+ _smartlinksProxyStream: true,
717
+ id,
718
+ phase: 'open'
719
+ }
720
+ ```
721
+
722
+ Event:
723
+
724
+ ```ts
725
+ {
726
+ _smartlinksProxyStream: true,
727
+ id,
728
+ phase: 'event',
729
+ data: parsedJsonEvent
730
+ }
731
+ ```
732
+
733
+ End:
734
+
735
+ ```ts
736
+ {
737
+ _smartlinksProxyStream: true,
738
+ id,
739
+ phase: 'end'
740
+ }
741
+ ```
742
+
743
+ Error:
744
+
745
+ ```ts
746
+ {
747
+ _smartlinksProxyStream: true,
748
+ id,
749
+ phase: 'error',
750
+ error: 'message',
751
+ status?: number
752
+ }
753
+ ```
754
+
755
+ ### What does not change
756
+
757
+ These parts of the parent iframe integration stay the same:
758
+
759
+ - normal `_smartlinksProxyRequest` request/response flow
760
+ - upload proxy flow
761
+ - auth login/logout postMessage handling
762
+ - route/deep-link handling
763
+ - resize handling
764
+
765
+ This is an additive protocol, not a replacement.
766
+
767
+ ### Current SDK reference
768
+
769
+ The SDK implementation lives in:
770
+
771
+ - [src/http.ts](src/http.ts)
772
+ - [src/iframeResponder.ts](src/iframeResponder.ts)
773
+ - [src/types/iframeResponder.ts](src/types/iframeResponder.ts)
774
+ - [src/api/ai.ts](src/api/ai.ts)
775
+
776
+ ### Practical recommendation
777
+
778
+ If your parent already uses `IframeResponder`, prefer upgrading to the SDK version with these changes instead of re-implementing the protocol manually.
779
+
780
+ If your parent has a custom iframe bridge, implement exactly the three new message types above and reuse your existing auth/header logic from normal proxied requests.
@@ -135,7 +135,7 @@ admin API fires the flow automatically — no manual run needed.
135
135
  | `integrations.deleteFlow(collectionId, id)` | `DELETE /integrations/flows/:id` |
136
136
  | `integrations.runFlow(collectionId, id, opts?)` | `POST /integrations/flows/:id/run` |
137
137
  | `secrets.list(collectionId, query?)` | `GET /secrets` |
138
- | `secrets.set(collectionId, input)` | `POST /secrets` |
138
+ | `secrets.set(collectionId, input)` | `POST /secrets` (generated ref — for flows; an app's server functions use `secrets.put`, see server-functions.md) |
139
139
  | `secrets.get(collectionId, ref)` | `GET /secrets/:ref` |
140
140
  | `secrets.rotate(collectionId, ref, input)` | `PUT /secrets/:ref` |
141
141
  | `secrets.remove(collectionId, ref)` | `DELETE /secrets/:ref` |