Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ All notable changes to the `stream-connect-sdk` npm package. The
companion React Native hook (`stream-connect-sdk-hook`) is on its own
release line; see [`sdk-hook/docs/README.md`](./sdk-hook/docs/README.md).

## 0.8.1

### Handle expired connectAccessToken without a confusing error

Connect access tokens have a ~60 minute server-side TTL. Previously,
when a member left an SDK-hosting page open past that window and came
back to interact, the next API call failed with a misleading 422
error ("A connect access token can only be used once...") that
pointed integrators toward the wrong fix.

The SDK now detects the backend's expired-token shape
(`{status: 422, error_code: "expired_connect_token"}`) via an axios
response interceptor. **What happens next depends on how you wire
it** — there's no zero-config auto-recovery because only your server
holds the SDK secret key needed to mint a new token:

* **Recommended: wire `connectAccessTokenRefreshFn`** — a new init
option that points at a refresh endpoint on your server. The SDK
calls it for a fresh token, swaps it into the
`X-Connect-Access-Token` header, and retries the failed request
transparently — the member sees no error. Multiple parallel failed
requests share a single refresh attempt (stampede guard). See
[`docs/connect-access-token.md` → Refreshing an expired
token](./docs/connect-access-token.md#refreshing-an-expired-token-081)
for the server-side endpoint pattern (Flask + Express examples)
and the SDK-side wiring.
* **Fallback: wire `onConnectAccessTokenExpired`** — a new init
callback that fires when no refresh hook is wired or the refresh
rejected. Use to render a "session expired, please reload" UI on
the host page. Also dispatched as a
`tpastream-connect-token-expired` CustomEvent on `window` for
global listeners. Coalesced to one notification per expiry cycle.
* **No wiring at all**: the only change you see is a cleaner
server-side error message in the existing error-handler chain
(`handleFormErrors` etc.). The 422 still bubbles up; the member
still has to reload the page to recover.

See [`docs/client-usage.md`](./docs/client-usage.md) for the option
reference.

## 0.8.0

### Look and feel
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## Version

### 0.8.1

Handle expired `connectAccessToken` cleanly. Long-lived pages no longer surface the misleading 422 when the ~60-minute server-side TTL elapses; integrations opt into transparent refresh via a new server-side endpoint hook, or a clean expiry callback as a fallback. See the [Refreshing an expired token](./docs/connect-access-token.md#refreshing-an-expired-token-081) integration guide.

### 0.8.0

Polished default appearance, React 19 + TypeScript, real-time credential-validation streaming, and a substantial dependency cleanup. The init() contract is backward-compatible: every option supported in 0.7.7 keeps working, including the custom render props (`renderChoosePayer`, `renderPayerForm`, `renderEndWidget`).
Expand All @@ -17,6 +21,19 @@ This SDK embeds the [EasyEnrollment platform](https://www.easyenrollment.net) in
Latest highlights below. The full per-version changelog lives in
[CHANGELOG.md](./CHANGELOG.md).

### 0.8.1 highlights

* Handle expired `connectAccessToken` (the ~60-minute server-side
TTL) without the misleading 422. Opt in to transparent recovery by
wiring `connectAccessTokenRefreshFn` against a server-side refresh
endpoint (Flask + Express snippets in [docs](./docs/connect-access-token.md#refreshing-an-expired-token-081)),
or use the `onConnectAccessTokenExpired` callback (and
`tpastream-connect-token-expired` window event) to render a
"session expired" UI as a fallback. Parallel-request stampede
guarded; notifications coalesced to one per expiry cycle.
Integrations that wire nothing see a cleaner error message but
still need a page reload to recover.

### 0.8.0 highlights

* Polished default appearance; no host-page CSS required.
Expand Down
2 changes: 1 addition & 1 deletion assets/sdk/components/SDK.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { FixCredentials } from './FixCredentials';
import { SelectEnrollProcess } from './SelectEnrollProcess';
import { TermsOfUse } from './TermsOfUse';

const VERSION = '0.8.0';
const VERSION = '0.8.1';

interface SDKProps extends SDKInitOptions {
/** Computed inside the entry; passed in here so the controller
Expand Down
2 changes: 1 addition & 1 deletion assets/sdk/entries/sdk-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { SDKInitOptions } from '../types-init';
// stylesheet so customers driving their own UI via the renderXxx
// callbacks aren't forced to ship our ~50 KB of Tailwind output.

const VERSION = '0.8.0';
const VERSION = '0.8.1';

// Track one React root per container element. Some host pages call
// StreamConnect() more than once against the same `el` (e.g. on a
Expand Down
5 changes: 3 additions & 2 deletions docs/client-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Mount the SDK by calling `StreamConnect({...})` with your config. The minimum yo
### Via CDN (vanilla HTML, server-rendered, Backbone, jQuery)

```html
<script src="https://app.tpastream.com/static/js/sdk-v-0.8.0.js"></script>
<script src="https://app.tpastream.com/static/js/sdk-v-0.8.1.js"></script>
<div id="sdk-hook"></div>
<script>
window.StreamConnect({
Expand All @@ -29,7 +29,8 @@ Mount the SDK by calling `StreamConnect({...})` with your config. The minimum yo

Three CDN forms are supported:

* `sdk-v-0.8.0.js` (this version, pinned)
* `sdk-v-0.8.1.js` (current 0.8.x, pinned)
* `sdk-v-0.8.0.js` (0.8.0, pinned)
* `sdk-v-0.7.7.js` (last 0.7.x, pinned)
* `sdk.js` (floating pointer to the latest release; updates automatically when a new version ships)

Expand Down
138 changes: 137 additions & 1 deletion docs/connect-access-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,143 @@ StreamConnect({

This JWT expires after 1 hour.

## Mid-session token refresh (0.8+)
## Refreshing an expired token (0.8.1+)

Connect access tokens expire after 1 hour. When a member leaves your
SDK-hosting page open past that window and comes back to interact,
the next API call fails with a 422 and `error_code:
"expired_connect_token"`. The 0.8.1 SDK can recover transparently
**if** you wire a refresh hook — but the refresh has to go through
your server, because only your server holds the SDK secret key.

### Why this can't be automatic

The whole point of the connect access token is that the secret key
never reaches the browser. If the SDK could mint its own tokens, the
secret would have to be in JS code where any user with devtools could
read it. So the SDK can't refresh on its own — it can only ask your
server for a fresh token.

### Step 1: expose a refresh endpoint on your server

This endpoint does the same `POST` to `/api/create-connect-token`
that your original page-render did, returning the new JWT to the
browser. **Gate it with the same auth you use for the member's
session** — anyone who can call it can extend any member's SDK
session indefinitely.

**Flask:**

```python
from flask import jsonify, request
import requests

@app.route("/api/tpa-sdk-refresh-token", methods=["POST"])
@your_login_required
def tpa_sdk_refresh_token():
response = requests.post(
"https://app.tpastream.com/api/create-connect-token",
json={
"connect_access_key": app.config["TPA_SDK_PUBLIC_KEY"],
"connect_secret_key": app.config["TPA_SDK_SECRET_KEY"],
},
timeout=10,
)
response.raise_for_status()
return jsonify(token=response.json()["data"])
```

**Express:**

```js
app.post('/api/tpa-sdk-refresh-token', yourLoginRequired, async (req, res) => {
const r = await fetch('https://app.tpastream.com/api/create-connect-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
connect_access_key: process.env.TPA_SDK_PUBLIC_KEY,
connect_secret_key: process.env.TPA_SDK_SECRET_KEY,
}),
});
if (!r.ok) return res.status(502).json({ error: 'refresh failed' });
const { data } = await r.json();
res.json({ token: data });
});
```

**ASP.NET Core (C#):**

```csharp
[Authorize] // gate with your member-session auth
[HttpPost("api/tpa-sdk-refresh-token")]
public async Task<IActionResult> TpaSdkRefreshToken(
[FromServices] IHttpClientFactory clientFactory,
[FromServices] IConfiguration config)
{
var http = clientFactory.CreateClient();
var payload = new
{
connect_access_key = config["TpaSdk:PublicKey"],
connect_secret_key = config["TpaSdk:SecretKey"],
};
using var response = await http.PostAsJsonAsync(
"https://app.tpastream.com/api/create-connect-token", payload);
if (!response.IsSuccessStatusCode)
return StatusCode(502, new { error = "refresh failed" });
var doc = await response.Content.ReadFromJsonAsync<JsonElement>();
return Ok(new { token = doc.GetProperty("data").GetString() });
}
```

The pattern is the same in every backend: an authenticated POST that proxies to `app.tpastream.com/api/create-connect-token` with your stored secret + public keys. PHP, Ruby, Go, Java — same shape, swap the HTTP client for whatever's idiomatic.

### Step 2: wire `connectAccessTokenRefreshFn` on the SDK

```js
StreamConnect({
// ... your existing config ...
connectAccessToken: '<initial-token-from-page-render>',
connectAccessTokenRefreshFn: async () => {
const r = await fetch('/api/tpa-sdk-refresh-token', {
method: 'POST',
credentials: 'same-origin',
});
if (!r.ok) throw new Error(`refresh failed: ${r.status}`);
return (await r.json()).token;
},
});
```

When the SDK detects an expired token, it calls this function, swaps
the new value into its `X-Connect-Access-Token` header, and retries
the failed request — the member sees no error. Multiple parallel
failed requests share a single refresh attempt (stampede guard) so
your endpoint gets called once per expiry cycle, not once per request.

### Fallback: notification-only

If you can't add a refresh endpoint (legacy host, static site, etc.),
wire `onConnectAccessTokenExpired` instead. The SDK will surface the
expiry to your callback (and dispatch a
`tpastream-connect-token-expired` CustomEvent on `window`) so you can
prompt the member to reload:

```js
StreamConnect({
// ... your existing config ...
onConnectAccessTokenExpired: () => {
if (confirm('Your session has expired. Reload to continue?')) {
window.location.reload();
}
},
});
```

The reload triggers a fresh page render, which mints a fresh
`connectAccessToken` server-side. The trade-off versus the refresh
hook is that the member loses any in-progress credential entry.

## Mid-session token refresh (Patient Access API redirect)

After a Patient Access API redirect completes, `app.tpastream.com`
appends a fresh `?accessToken=...` query parameter to the return
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Here is what the test htmlscript looks like.
```

`<script src="https://app.tpastream.com/static/js/sdk.js"></script>` in the head will bring down the latest version of the StreamConnect SDK. If you need to pin to a specific version, change the src to something like
`<script src="https://app.tpastream.com/static/js/sdk-v-0.8.0.js"></script>`. Pinned versions remain available indefinitely.
`<script src="https://app.tpastream.com/static/js/sdk-v-0.8.1.js"></script>`. Pinned versions remain available indefinitely.

The 0.8 SDK is visually self-contained: it does not require Bootstrap, jQuery, FontAwesome, or any other host-page CSS. Host pages that already load Bootstrap can keep doing so without conflict. SDK styles use a `tpa-` class prefix to avoid name collisions with host CSS, and the reset/theme variables are wrapped under `.tpa-sdk-root` so they only affect the SDK subtree.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stream-connect-sdk",
"version": "0.8.0",
"version": "0.8.1",
"description": "A JavaScript SDK implementing TPAStream's Connect Platform",
"scripts": {
"build": "webpack --config webpack.prod-sdk.js --mode=production",
Expand Down
2 changes: 1 addition & 1 deletion sdk-hook/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ that bootstraps the SDK with your tokens server-side:
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Pin to a specific version. -->
<script src="https://app.tpastream.com/static/js/sdk-v-0.8.0.js"></script>
<script src="https://app.tpastream.com/static/js/sdk-v-0.8.1.js"></script>
</head>
<body>
<div id="sdk-hook"></div>
Expand Down
Loading