@proveanything/smartlinks 2.0.40 → 2.0.41
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/docs/API_SUMMARY.md +23 -1
- package/dist/docs/iframe-responder.md +310 -308
- package/dist/docs/server-functions.md +34 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +28 -4
- package/dist/openapi.yaml +32 -0
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.js +1 -0
- package/dist/types/appManifest.d.ts +31 -0
- package/docs/API_SUMMARY.md +23 -1
- package/docs/iframe-responder.md +310 -308
- package/docs/server-functions.md +34 -0
- package/openapi.yaml +32 -0
- package/package.json +1 -1
package/docs/iframe-responder.md
CHANGED
|
@@ -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.
|
package/docs/server-functions.md
CHANGED
|
@@ -324,6 +324,40 @@ name (a first-party builtin still wins). Always prefer an appId.
|
|
|
324
324
|
|
|
325
325
|
---
|
|
326
326
|
|
|
327
|
+
## Inputs — data handed to your function
|
|
328
|
+
|
|
329
|
+
Declare what your function needs and the platform fetches it **before** the run: read it from `ctx.inputs` instead of calling `ctx.sl` first. One less round trip — and for functions running in the isolated runner, one less network hop.
|
|
330
|
+
|
|
331
|
+
```jsonc
|
|
332
|
+
// app.manifest.json → functions.definitions[]
|
|
333
|
+
{
|
|
334
|
+
"name": "onProductChanged",
|
|
335
|
+
"trigger": { "type": "event", "eventTypes": ["product.updated"] },
|
|
336
|
+
"capabilities": ["sl:products:read", "sl:data:read"],
|
|
337
|
+
"inputs": { "appConfig": true } // entity is on by default for event triggers
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
export async function onProductChanged(ctx, event) {
|
|
343
|
+
const product = ctx.inputs.entity // the product that changed
|
|
344
|
+
const { interactionIds } = ctx.inputs.appConfig || {} // this app's config for the collection
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
| Input | What you get | Needs | Triggers |
|
|
349
|
+
|---|---|---|---|
|
|
350
|
+
| `entity` | The record the event is about: the product, your app's record, or the interaction event. **On by default**; `"entity": false` opts out. | read on that resource (`sl:products:read`, `sl:records:read`) | event |
|
|
351
|
+
| `collection` | The collection's public record | — | all |
|
|
352
|
+
| `appConfig` | Your app's config for this collection (where setup like `interactionIds` lives) | `sl:data:read` | all |
|
|
353
|
+
| `product` | A product named by the request: `{ "from": "body.productId" }`, `"query.<field>"` or `"path"` | `sl:products:read` | http |
|
|
354
|
+
|
|
355
|
+
- **Same rules as `ctx.sl`.** Inputs are fetched with your function's authority and capabilities, scoped to the collection it runs in — a product from another collection is never fetched.
|
|
356
|
+
- **Never an error.** Not found, not allowed or larger than 256 KB → that input is `null`. Check before you use it.
|
|
357
|
+
- **Current state.** Event inputs are fetched when your function runs, so you see the record as it is now; `event` still carries the ids.
|
|
358
|
+
- **Checked at publish.** An unknown input, a bad `from`, or a missing capability fails validation.
|
|
359
|
+
- **Testing:** pass `inputs` to the test context — `createFunctionTestContext({ def, inputs: { entity: { id: 'p1', name: 'Kettle' } } })`.
|
|
360
|
+
|
|
327
361
|
## Exposing a function to the AI agent
|
|
328
362
|
|
|
329
363
|
An `http` function can be offered to the AI agent as a **callable tool**, alongside the built-in
|