@mission_sciences/provider-sdk 0.1.2 → 0.2.0-dev.b8394a3
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/README.md +365 -416
- package/dist/index.d.ts +3 -1
- package/dist/marketplace-sdk.es.js +9 -5
- package/dist/marketplace-sdk.es.js.map +1 -1
- package/dist/marketplace-sdk.umd.js +1 -1
- package/dist/marketplace-sdk.umd.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,295 +7,381 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/@mission_sciences/provider-sdk)
|
|
8
8
|
[](https://github.com/Mission-Sciences/provider-sdk/actions)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## 🚀 Quick Start
|
|
10
|
+
## Quick Start
|
|
13
11
|
|
|
14
12
|
```bash
|
|
15
|
-
# Install
|
|
16
13
|
npm install @mission_sciences/provider-sdk
|
|
14
|
+
```
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
import MarketplaceSDK from '@mission_sciences/provider-sdk';
|
|
16
|
+
```javascript
|
|
17
|
+
import { MarketplaceSDK } from '@mission_sciences/provider-sdk';
|
|
20
18
|
|
|
21
19
|
const sdk = new MarketplaceSDK({
|
|
22
|
-
|
|
20
|
+
jwksUri: 'https://api.generalwisdom.com/.well-known/jwks.json',
|
|
23
21
|
applicationId: 'your-app-id',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
autoStart: true,
|
|
23
|
+
hooks: {
|
|
24
|
+
async onSessionStart(context) {
|
|
25
|
+
// context.jwt - raw JWT string
|
|
26
|
+
// context.userId - GW user ID
|
|
27
|
+
// context.email - user email
|
|
28
|
+
// context.sessionId, context.orgId, context.applicationId, etc.
|
|
29
|
+
|
|
30
|
+
// Exchange the GW JWT for your app's auth tokens
|
|
31
|
+
const res = await fetch('/auth/marketplace', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ jwt: context.jwt }),
|
|
35
|
+
});
|
|
36
|
+
const { token } = await res.json();
|
|
37
|
+
localStorage.setItem('auth_token', token);
|
|
38
|
+
},
|
|
39
|
+
async onSessionEnd(context) {
|
|
40
|
+
// context.reason = 'expired' | 'manual' | 'error'
|
|
41
|
+
localStorage.clear();
|
|
42
|
+
},
|
|
30
43
|
},
|
|
31
44
|
});
|
|
32
45
|
|
|
33
46
|
await sdk.initialize();
|
|
34
47
|
```
|
|
35
48
|
|
|
36
|
-
##
|
|
49
|
+
## Documentation
|
|
37
50
|
|
|
38
|
-
- **[Integration Guide](./INTEGRATION_GUIDE.md)**
|
|
39
|
-
- **[Quick Start Guide](./QUICKSTART.md)**
|
|
40
|
-
- **[
|
|
41
|
-
- **[
|
|
51
|
+
- **[Integration Guide](./INTEGRATION_GUIDE.md)** -- Comprehensive guide for all frameworks (vanilla JS, React, Vue, Chrome Extensions)
|
|
52
|
+
- **[Quick Start Guide](./QUICKSTART.md)** -- Get started in 3 minutes
|
|
53
|
+
- **[JWT Specification](./jwt-specification.md)** -- Token format and claim details
|
|
54
|
+
- **[Validation Guide](./VALIDATION.md)** -- Testing and validation strategies
|
|
55
|
+
- **[Auth Integration Demo](./examples/auth-integration/)** -- Full working demo with 5 identity providers and 2 frontend implementations
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
## Features
|
|
44
58
|
|
|
45
|
-
|
|
59
|
+
### Core
|
|
60
|
+
- **Zero Config**: Extracts JWT from URL, validates via JWKS, starts session timer
|
|
61
|
+
- **Framework Agnostic**: Works with vanilla JS, React, Vue, or any framework
|
|
62
|
+
- **TypeScript First**: Full type definitions with all interfaces exported
|
|
63
|
+
- **Lightweight**: Single dependency (`jose` for JWT/JWKS)
|
|
64
|
+
- **Secure**: RS256 JWT verification with JWKS rotation support
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
### Lifecycle Hooks
|
|
67
|
+
- **`onSessionStart`** -- Fires after JWT validation, before timer starts. Use to exchange tokens with your auth system. Hook failure prevents session start (strict mode).
|
|
68
|
+
- **`onSessionEnd`** -- Fires on expiration or manual end. Use to revoke sessions. Errors are logged but don't block teardown (lenient mode).
|
|
69
|
+
- **`onSessionWarning`** -- Fires when session nears expiration (configurable threshold).
|
|
70
|
+
- **`onSessionExtend`** -- Fires after session extension. Use to refresh auth tokens.
|
|
48
71
|
|
|
49
|
-
###
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
72
|
+
### Advanced (Phase 2)
|
|
73
|
+
- **Heartbeat**: Automatic server sync at configurable intervals
|
|
74
|
+
- **Multi-Tab Sync**: Master tab election via BroadcastChannel API
|
|
75
|
+
- **Session Extension**: Self-service renewal with `extendSession(minutes)`
|
|
76
|
+
- **Early Completion**: End sessions early with `completeSession(actualMinutes)`
|
|
77
|
+
- **Visibility API**: Auto-pause timer when tab is hidden
|
|
78
|
+
- **Backend Validation**: Alternative to JWKS for sensitive apps
|
|
56
79
|
|
|
57
|
-
|
|
58
|
-
- ❤️ **Heartbeat System**: Automatic server sync
|
|
59
|
-
- 🔄 **Multi-Tab Sync**: Master tab election with BroadcastChannel API
|
|
60
|
-
- ⏰ **Session Extension**: Self-service renewal
|
|
61
|
-
- ✅ **Early Completion**: End sessions early with refund calculation
|
|
62
|
-
- 👁️ **Visibility API**: Auto-pause when tab hidden
|
|
63
|
-
- 🔐 **Backend Validation**: Alternative to JWKS for sensitive apps
|
|
64
|
-
|
|
65
|
-
## 🎯 How It Works
|
|
80
|
+
## How It Works
|
|
66
81
|
|
|
67
82
|
```
|
|
68
|
-
Marketplace
|
|
83
|
+
Marketplace --> JWT in URL --> SDK validates via JWKS --> Lifecycle hooks fire --> Session active
|
|
69
84
|
```
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
5. **Session Active**: User interacts with your app
|
|
78
|
-
6. **Warning**: Alert at 5 minutes remaining (configurable)
|
|
79
|
-
7. **Session End**: Calls your `onSessionEnd` hook
|
|
80
|
-
8. **Redirect**: Returns to marketplace (optional)
|
|
81
|
-
|
|
82
|
-
## 🔒 Secure Publishing & Provenance
|
|
86
|
+
1. User launches your app from the marketplace with `?gwSession=<token>` in the URL (parameter name configurable via `jwtParamName`)
|
|
87
|
+
2. SDK extracts the JWT and verifies it against the JWKS endpoint (RS256)
|
|
88
|
+
3. `hooks.onSessionStart` fires with the validated session context
|
|
89
|
+
4. Session timer starts counting down
|
|
90
|
+
5. `hooks.onSessionWarning` fires at the configured threshold
|
|
91
|
+
6. When the timer expires or `endSession()` is called, `hooks.onSessionEnd` fires
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- **Dual Publishing**: Available on both [npm](https://www.npmjs.com/package/@mission_sciences/provider-sdk) (public) and AWS CodeArtifact (private)
|
|
87
|
-
- **Cryptographic Signatures**: All releases signed with GitHub Actions OIDC
|
|
88
|
-
- **Provenance Transparency**: Build provenance recorded in [Sigstore transparency log](https://search.sigstore.dev)
|
|
89
|
-
- **No Hardcoded Secrets**: CI/CD uses OIDC for AWS and npm authentication
|
|
90
|
-
- **Automated CI/CD**: GitHub Actions workflow with comprehensive testing and security checks
|
|
93
|
+
## Installation
|
|
91
94
|
|
|
92
|
-
Verify package provenance:
|
|
93
95
|
```bash
|
|
94
|
-
npm
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
## 📦 Installation
|
|
96
|
+
# npm (public registry)
|
|
97
|
+
npm install @mission_sciences/provider-sdk
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
# yarn
|
|
100
|
+
yarn add @mission_sciences/provider-sdk
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
# pnpm
|
|
103
|
+
pnpm add @mission_sciences/provider-sdk
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
### AWS CodeArtifact (Private Registry)
|
|
106
107
|
|
|
107
108
|
```bash
|
|
108
|
-
# Configure CodeArtifact
|
|
109
109
|
aws codeartifact login \
|
|
110
110
|
--tool npm \
|
|
111
111
|
--domain general-wisdom-dev \
|
|
112
112
|
--repository sdk-packages \
|
|
113
113
|
--region us-east-1
|
|
114
114
|
|
|
115
|
-
# Install
|
|
116
115
|
npm install @mission_sciences/provider-sdk
|
|
117
116
|
```
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
yarn add @mission_sciences/provider-sdk
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### PNPM
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
pnpm add @mission_sciences/provider-sdk
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## 🏗️ Basic Usage
|
|
118
|
+
## Usage
|
|
132
119
|
|
|
133
120
|
### Vanilla JavaScript
|
|
134
121
|
|
|
135
122
|
```javascript
|
|
136
|
-
import MarketplaceSDK from '@mission_sciences/provider-sdk';
|
|
123
|
+
import { MarketplaceSDK } from '@mission_sciences/provider-sdk';
|
|
137
124
|
|
|
138
125
|
const sdk = new MarketplaceSDK({
|
|
139
|
-
|
|
126
|
+
jwksUri: '/.well-known/jwks.json',
|
|
140
127
|
applicationId: 'my-app',
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
document.getElementById('app').style.display = 'none';
|
|
128
|
+
autoStart: true,
|
|
129
|
+
warningThresholdSeconds: 120,
|
|
130
|
+
hooks: {
|
|
131
|
+
async onSessionStart(context) {
|
|
132
|
+
// Exchange GW JWT for your app's tokens
|
|
133
|
+
const res = await fetch('/auth/exchange', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ jwt: context.jwt }),
|
|
137
|
+
});
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
sessionStorage.setItem('access_token', data.access_token);
|
|
140
|
+
},
|
|
141
|
+
async onSessionEnd(context) {
|
|
142
|
+
sessionStorage.clear();
|
|
143
|
+
},
|
|
158
144
|
},
|
|
159
145
|
});
|
|
160
146
|
|
|
147
|
+
// Event handlers (separate from hooks -- these fire after hooks complete)
|
|
148
|
+
sdk.on('onSessionStart', (sessionData) => {
|
|
149
|
+
document.getElementById('app').style.display = 'block';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
sdk.on('onSessionEnd', () => {
|
|
153
|
+
document.getElementById('app').style.display = 'none';
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
sdk.on('onError', (error) => {
|
|
157
|
+
console.error('SDK error:', error.message);
|
|
158
|
+
});
|
|
159
|
+
|
|
161
160
|
await sdk.initialize();
|
|
162
161
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
// Timer display
|
|
163
|
+
setInterval(() => {
|
|
164
|
+
document.getElementById('timer').textContent = sdk.getFormattedTime();
|
|
165
|
+
}, 1000);
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
### React
|
|
169
169
|
|
|
170
170
|
```typescript
|
|
171
|
-
import { useEffect } from 'react';
|
|
172
|
-
import MarketplaceSDK from '@mission_sciences/provider-sdk';
|
|
171
|
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
172
|
+
import { MarketplaceSDK } from '@mission_sciences/provider-sdk';
|
|
173
173
|
|
|
174
174
|
function useMarketplaceSession() {
|
|
175
|
+
const [session, setSession] = useState(null);
|
|
176
|
+
const [loading, setLoading] = useState(true);
|
|
177
|
+
const [time, setTime] = useState('--:--');
|
|
178
|
+
const sdkRef = useRef(null);
|
|
179
|
+
|
|
175
180
|
useEffect(() => {
|
|
176
181
|
const sdk = new MarketplaceSDK({
|
|
177
|
-
|
|
182
|
+
jwksUri: '/.well-known/jwks.json',
|
|
178
183
|
applicationId: 'my-react-app',
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// Clear auth state
|
|
188
|
-
await logout();
|
|
184
|
+
autoStart: true,
|
|
185
|
+
hooks: {
|
|
186
|
+
async onSessionStart(context) {
|
|
187
|
+
await authenticateUser(context.jwt);
|
|
188
|
+
},
|
|
189
|
+
async onSessionEnd(context) {
|
|
190
|
+
await logout();
|
|
191
|
+
},
|
|
189
192
|
},
|
|
190
193
|
});
|
|
191
194
|
|
|
195
|
+
sdk.on('onSessionStart', (data) => {
|
|
196
|
+
setSession(data);
|
|
197
|
+
setLoading(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
sdk.on('onError', (err) => {
|
|
201
|
+
console.error(err);
|
|
202
|
+
setLoading(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
sdkRef.current = sdk;
|
|
192
206
|
sdk.initialize();
|
|
193
207
|
|
|
194
|
-
|
|
208
|
+
const interval = setInterval(() => {
|
|
209
|
+
if (sdkRef.current) setTime(sdkRef.current.getFormattedTime());
|
|
210
|
+
}, 1000);
|
|
211
|
+
|
|
212
|
+
return () => {
|
|
213
|
+
clearInterval(interval);
|
|
214
|
+
sdk.destroy();
|
|
215
|
+
};
|
|
195
216
|
}, []);
|
|
217
|
+
|
|
218
|
+
return { session, loading, time, sdk: sdkRef };
|
|
196
219
|
}
|
|
197
220
|
```
|
|
198
221
|
|
|
199
|
-
See [
|
|
222
|
+
See the [Integration Guide](./INTEGRATION_GUIDE.md) for Vue, Chrome Extensions, and more patterns.
|
|
200
223
|
|
|
201
|
-
|
|
224
|
+
### Auth Integration Demo
|
|
202
225
|
|
|
203
|
-
|
|
226
|
+
A complete working example with Docker, 5 identity providers, and 2 frontend implementations lives in `examples/auth-integration/`:
|
|
204
227
|
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
```bash
|
|
229
|
+
# Build the SDK first
|
|
230
|
+
npm run build
|
|
231
|
+
|
|
232
|
+
# Start the demo (defaults to mock IdP + React storefront + Auth0 protocol)
|
|
233
|
+
cd examples/auth-integration
|
|
234
|
+
cp .env.mock .env
|
|
235
|
+
docker compose up --build
|
|
212
236
|
|
|
213
|
-
|
|
237
|
+
# Open http://localhost:8080/generate-test-url
|
|
214
238
|
```
|
|
215
239
|
|
|
216
|
-
|
|
240
|
+
The demo includes:
|
|
241
|
+
- **Mock IdP** that speaks Auth0, Okta, Azure AD, and Cognito protocols
|
|
242
|
+
- **React ecommerce storefront** with product shop, cart, role-gated admin panel
|
|
243
|
+
- **Vanilla JS reference** with auth hooks and event log
|
|
244
|
+
- **Backend** with provider-agnostic auth exchange using the `AuthProvider` interface
|
|
245
|
+
- **Role-based access control** showing `gw-user` vs `org-admin` UI gating
|
|
217
246
|
|
|
218
|
-
|
|
219
|
-
.gw-session-header {
|
|
220
|
-
background: #1a1a1a;
|
|
221
|
-
padding: 12px 24px;
|
|
222
|
-
}
|
|
247
|
+
See [examples/auth-integration/README.md](./examples/auth-integration/README.md) for full documentation, and [examples/auth-integration/docs/DEMO_WALKTHROUGH.md](./examples/auth-integration/docs/DEMO_WALKTHROUGH.md) for a step-by-step walkthrough.
|
|
223
248
|
|
|
224
|
-
|
|
225
|
-
font-size: 18px;
|
|
226
|
-
color: #00ff88;
|
|
227
|
-
}
|
|
249
|
+
## Configuration
|
|
228
250
|
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
```typescript
|
|
252
|
+
interface SDKConfig {
|
|
253
|
+
// JWT & Validation
|
|
254
|
+
jwksUri?: string; // JWKS endpoint (default: GW production endpoint)
|
|
255
|
+
jwtParamName?: string; // URL query parameter name (default: 'gwSession')
|
|
256
|
+
applicationId?: string; // Your application ID
|
|
257
|
+
useBackendValidation?: boolean; // Use backend instead of JWKS (default: false)
|
|
258
|
+
|
|
259
|
+
// Session Behavior
|
|
260
|
+
autoStart?: boolean; // Auto-start from URL JWT (default: true)
|
|
261
|
+
warningThresholdSeconds?: number; // Warning before expiry (default: 300)
|
|
262
|
+
marketplaceUrl?: string; // Redirect URL after session end
|
|
263
|
+
|
|
264
|
+
// Lifecycle Hooks
|
|
265
|
+
hooks?: {
|
|
266
|
+
onSessionStart?: (ctx: SessionStartContext) => Promise<void> | void;
|
|
267
|
+
onSessionEnd?: (ctx: SessionEndContext) => Promise<void> | void;
|
|
268
|
+
onSessionExtend?: (ctx: SessionExtendContext) => Promise<void> | void;
|
|
269
|
+
onSessionWarning?: (ctx: SessionWarningContext) => Promise<void> | void;
|
|
270
|
+
};
|
|
271
|
+
hookTimeoutMs?: number; // Hook timeout (default: 5000)
|
|
272
|
+
|
|
273
|
+
// Phase 2 Features
|
|
274
|
+
enableHeartbeat?: boolean; // Server heartbeat (default: false)
|
|
275
|
+
heartbeatIntervalSeconds?: number; // Heartbeat interval (default: 30)
|
|
276
|
+
enableTabSync?: boolean; // Multi-tab sync (default: false)
|
|
277
|
+
pauseOnHidden?: boolean; // Pause when tab hidden (default: false)
|
|
278
|
+
|
|
279
|
+
// UI
|
|
280
|
+
themeMode?: 'light' | 'dark' | 'auto';
|
|
281
|
+
debug?: boolean; // Console logging (default: false)
|
|
231
282
|
}
|
|
232
283
|
```
|
|
233
284
|
|
|
234
|
-
##
|
|
285
|
+
## API Reference
|
|
235
286
|
|
|
236
|
-
###
|
|
287
|
+
### MarketplaceSDK
|
|
237
288
|
|
|
238
289
|
```typescript
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const response = await fetch('https://api.your-app.com/auth/marketplace', {
|
|
242
|
-
method: 'POST',
|
|
243
|
-
headers: {
|
|
244
|
-
'Authorization': `Bearer ${context.jwt}`,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const { token } = await response.json();
|
|
249
|
-
|
|
250
|
-
// Store your app's auth token
|
|
251
|
-
localStorage.setItem('auth_token', token);
|
|
252
|
-
},
|
|
253
|
-
```
|
|
290
|
+
class MarketplaceSDK {
|
|
291
|
+
constructor(config: SDKConfig)
|
|
254
292
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
```python
|
|
258
|
-
@app.route('/auth/marketplace', methods=['POST'])
|
|
259
|
-
def marketplace_auth():
|
|
260
|
-
jwt_token = request.headers.get('Authorization').replace('Bearer ', '')
|
|
261
|
-
|
|
262
|
-
# Validate JWT
|
|
263
|
-
claims = validate_marketplace_jwt(jwt_token)
|
|
264
|
-
|
|
265
|
-
# Create/update user
|
|
266
|
-
user = get_or_create_user(claims['userId'], claims.get('email'))
|
|
267
|
-
|
|
268
|
-
# Generate your app's token
|
|
269
|
-
app_token = generate_app_token(user)
|
|
270
|
-
|
|
271
|
-
return jsonify({'token': app_token})
|
|
272
|
-
```
|
|
293
|
+
// Initialization
|
|
294
|
+
async initialize(): Promise<SessionData>
|
|
273
295
|
|
|
274
|
-
|
|
296
|
+
// Event handlers (fire after hooks complete)
|
|
297
|
+
on(event: 'onSessionStart', handler: (data: SessionData) => void): void
|
|
298
|
+
on(event: 'onSessionEnd', handler: () => void): void
|
|
299
|
+
on(event: 'onSessionWarning', handler: (data: { remainingSeconds: number }) => void): void
|
|
300
|
+
on(event: 'onError', handler: (error: Error) => void): void
|
|
275
301
|
|
|
276
|
-
|
|
302
|
+
// Timer
|
|
303
|
+
startTimer(): void
|
|
304
|
+
pauseTimer(): void
|
|
305
|
+
resumeTimer(): void
|
|
306
|
+
isTimerRunning(): boolean
|
|
307
|
+
getRemainingTime(): number // Seconds remaining
|
|
308
|
+
getFormattedTime(): string // "M:SS" format
|
|
309
|
+
getFormattedTimeWithHours(): string // "H:MM:SS" format
|
|
277
310
|
|
|
278
|
-
|
|
311
|
+
// Session control
|
|
312
|
+
async endSession(): Promise<void>
|
|
313
|
+
async extendSession(additionalMinutes: number): Promise<void>
|
|
314
|
+
async completeSession(actualUsageMinutes?: number): Promise<void>
|
|
315
|
+
|
|
316
|
+
// Data
|
|
317
|
+
getSessionData(): SessionData | null
|
|
318
|
+
|
|
319
|
+
// Cleanup
|
|
320
|
+
destroy(): void
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Context Types
|
|
279
325
|
|
|
280
326
|
```typescript
|
|
281
|
-
interface
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
interface SessionStartContext {
|
|
328
|
+
sessionId: string;
|
|
329
|
+
userId: string;
|
|
330
|
+
email?: string;
|
|
331
|
+
orgId: string;
|
|
332
|
+
applicationId: string;
|
|
333
|
+
durationMinutes: number;
|
|
334
|
+
expiresAt: number; // Unix seconds
|
|
335
|
+
jwt: string; // Raw JWT for backend exchange
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface SessionEndContext {
|
|
339
|
+
sessionId: string;
|
|
340
|
+
userId: string;
|
|
341
|
+
reason: 'expired' | 'manual' | 'error';
|
|
342
|
+
actualDurationMinutes?: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface SessionExtendContext {
|
|
346
|
+
sessionId: string;
|
|
347
|
+
userId: string;
|
|
348
|
+
additionalMinutes: number;
|
|
349
|
+
newExpiresAt: number;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
interface SessionWarningContext {
|
|
353
|
+
sessionId: string;
|
|
354
|
+
userId: string;
|
|
355
|
+
remainingSeconds: number;
|
|
296
356
|
}
|
|
297
357
|
```
|
|
298
358
|
|
|
359
|
+
### Exports
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// Main class
|
|
363
|
+
export { MarketplaceSDK } from '@mission_sciences/provider-sdk';
|
|
364
|
+
export { MarketplaceSDK as default } from '@mission_sciences/provider-sdk';
|
|
365
|
+
|
|
366
|
+
// UI components
|
|
367
|
+
export { SessionHeader } from '@mission_sciences/provider-sdk';
|
|
368
|
+
export { WarningModal } from '@mission_sciences/provider-sdk';
|
|
369
|
+
|
|
370
|
+
// Core utilities
|
|
371
|
+
export { JWTParser, JWKSValidator, TimerManager } from '@mission_sciences/provider-sdk';
|
|
372
|
+
export { HeartbeatManager, TabSyncManager } from '@mission_sciences/provider-sdk';
|
|
373
|
+
|
|
374
|
+
// Theming
|
|
375
|
+
export { lightTheme, darkTheme, getTheme, generateCSSVariables } from '@mission_sciences/provider-sdk';
|
|
376
|
+
|
|
377
|
+
// Types
|
|
378
|
+
export type {
|
|
379
|
+
SDKConfig, SessionData, SDKEvents,
|
|
380
|
+
SessionStartContext, SessionEndContext, SessionExtendContext, SessionWarningContext,
|
|
381
|
+
SessionLifecycleHooks, ThemeMode,
|
|
382
|
+
} from '@mission_sciences/provider-sdk';
|
|
383
|
+
```
|
|
384
|
+
|
|
299
385
|
### JWT Structure
|
|
300
386
|
|
|
301
387
|
```json
|
|
@@ -308,11 +394,15 @@ interface SDKOptions {
|
|
|
308
394
|
"startTime": 1763599337,
|
|
309
395
|
"durationMinutes": 60,
|
|
310
396
|
"exp": 1763602937,
|
|
311
|
-
"iat": 1763599337
|
|
397
|
+
"iat": 1763599337,
|
|
398
|
+
"iss": "generalwisdom.com",
|
|
399
|
+
"sub": "a47884c8-50d1-7040-2de8-b7801699643c"
|
|
312
400
|
}
|
|
313
401
|
```
|
|
314
402
|
|
|
315
|
-
|
|
403
|
+
> **Note:** `email` is optional. It is included when available from the user's identity provider but may not be present in all JWTs. Always check for its presence before using it.
|
|
404
|
+
|
|
405
|
+
## Testing
|
|
316
406
|
|
|
317
407
|
### Generate Test JWT
|
|
318
408
|
|
|
@@ -321,278 +411,137 @@ npm run generate-keys # Create RSA key pair (dev only)
|
|
|
321
411
|
npm run generate-jwt 60 # Generate 60-minute JWT
|
|
322
412
|
```
|
|
323
413
|
|
|
324
|
-
###
|
|
414
|
+
### Dev Server
|
|
325
415
|
|
|
326
416
|
```bash
|
|
327
417
|
npm run test-server # Start dev server at localhost:3000
|
|
418
|
+
npm run test-server-p2 # Phase 2 dev server with heartbeat/tab-sync
|
|
328
419
|
```
|
|
329
420
|
|
|
330
|
-
Open: `http://localhost:3000?
|
|
421
|
+
Open: `http://localhost:3000?gwSession=<YOUR_JWT>`
|
|
331
422
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
## 🛠️ Development
|
|
335
|
-
|
|
336
|
-
### Build
|
|
423
|
+
### Unit Tests
|
|
337
424
|
|
|
338
425
|
```bash
|
|
339
|
-
npm run
|
|
340
|
-
npm run
|
|
426
|
+
npm run test # Run all tests
|
|
427
|
+
npm run test:watch # Watch mode
|
|
428
|
+
npm run test:coverage # Coverage report
|
|
429
|
+
npm run test:integration # Integration tests
|
|
341
430
|
```
|
|
342
431
|
|
|
343
|
-
|
|
432
|
+
## Development
|
|
344
433
|
|
|
345
434
|
```bash
|
|
435
|
+
npm run build # Build for production (tsc + vite)
|
|
436
|
+
npm run dev # Vite dev server with HMR
|
|
346
437
|
npm run lint # ESLint
|
|
347
438
|
npm run format # Prettier
|
|
348
|
-
npm run type-check # TypeScript
|
|
349
439
|
```
|
|
350
440
|
|
|
351
|
-
###
|
|
441
|
+
### Build Output
|
|
352
442
|
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
443
|
+
```
|
|
444
|
+
dist/
|
|
445
|
+
├── marketplace-sdk.es.js # ESM bundle
|
|
446
|
+
├── marketplace-sdk.es.js.map
|
|
447
|
+
├── marketplace-sdk.umd.js # UMD bundle
|
|
448
|
+
├── marketplace-sdk.umd.js.map
|
|
449
|
+
└── index.d.ts # TypeScript declarations
|
|
357
450
|
```
|
|
358
451
|
|
|
359
|
-
##
|
|
360
|
-
|
|
361
|
-
### GitHub Actions Workflow
|
|
362
|
-
|
|
363
|
-
The package is built and published using a comprehensive 8-job GitHub Actions pipeline:
|
|
364
|
-
|
|
365
|
-
1. **Test & Build** - Unit tests, type checking, linting, and production build
|
|
366
|
-
2. **Terraform Plan** - Review infrastructure changes (CodeArtifact setup)
|
|
367
|
-
3. **Terraform Apply** - Create/update AWS infrastructure
|
|
368
|
-
4. **Publish CodeArtifact** - Publish to private AWS registry
|
|
369
|
-
5. **Verify CodeArtifact** - Confirm successful publication
|
|
370
|
-
6. **Publish npm** - Publish to public npm with provenance
|
|
371
|
-
7. **Verify npm** - Confirm successful publication
|
|
372
|
-
8. **Create Release** - Generate GitHub release with artifacts
|
|
373
|
-
|
|
374
|
-
**Authentication:**
|
|
375
|
-
- AWS: OIDC via IAM role `GitHubActions-ProviderSDK` (no access keys)
|
|
376
|
-
- npm: Trusted Publishing with cryptographic provenance (no tokens)
|
|
377
|
-
|
|
378
|
-
### Planning Documentation
|
|
379
|
-
|
|
380
|
-
Comprehensive migration and setup documentation available in `planning/`:
|
|
381
|
-
|
|
382
|
-
- **[PROJECT_CONTEXT.md](./planning/PROJECT_CONTEXT.md)** - Project overview and context
|
|
383
|
-
- **[EXISTING_ANALYSIS.md](./planning/EXISTING_ANALYSIS.md)** - Codebase analysis
|
|
384
|
-
- **[REQUIREMENTS.md](./planning/REQUIREMENTS.md)** - Migration requirements
|
|
385
|
-
- **[CI_CD_ARCHITECTURE.md](./planning/CI_CD_ARCHITECTURE.md)** - Workflow design
|
|
386
|
-
- **[AWS_OIDC_SETUP.md](./planning/AWS_OIDC_SETUP.md)** - AWS OIDC configuration
|
|
387
|
-
- **[NPM_TRUSTED_PUBLISHING_SETUP.md](./planning/NPM_TRUSTED_PUBLISHING_SETUP.md)** - npm provenance setup
|
|
388
|
-
- **[GITHUB_SETUP_GUIDE.md](./planning/GITHUB_SETUP_GUIDE.md)** - Complete setup guide
|
|
389
|
-
- **[MIGRATION_CHECKLIST.md](./planning/MIGRATION_CHECKLIST.md)** - Migration checklist
|
|
452
|
+
## Infrastructure & CI/CD
|
|
390
453
|
|
|
391
|
-
|
|
454
|
+
The package is built and published using an 8-job GitHub Actions pipeline:
|
|
392
455
|
|
|
393
|
-
|
|
456
|
+
1. **Test & Build** -- Unit tests, type checking, linting, production build
|
|
457
|
+
2. **Terraform Plan** -- Review infrastructure changes (CodeArtifact)
|
|
458
|
+
3. **Terraform Apply** -- Create/update AWS infrastructure
|
|
459
|
+
4. **Publish CodeArtifact** -- Publish to private AWS registry
|
|
460
|
+
5. **Verify CodeArtifact** -- Confirm publication
|
|
461
|
+
6. **Publish npm** -- Publish to public npm with provenance
|
|
462
|
+
7. **Verify npm** -- Confirm publication
|
|
463
|
+
8. **Create Release** -- GitHub release with artifacts
|
|
394
464
|
|
|
395
|
-
|
|
396
|
-
class MarketplaceSDK {
|
|
397
|
-
constructor(options: SDKOptions)
|
|
398
|
-
|
|
399
|
-
// Initialize SDK
|
|
400
|
-
async initialize(): Promise<void>
|
|
401
|
-
|
|
402
|
-
// Session management
|
|
403
|
-
hasActiveSession(): boolean
|
|
404
|
-
getSession(): Session | null
|
|
405
|
-
async endSession(reason: string): Promise<void>
|
|
406
|
-
async extendSession(minutes: number): Promise<void>
|
|
407
|
-
|
|
408
|
-
// UI components
|
|
409
|
-
createSessionHeader(options?: HeaderOptions): SessionHeader
|
|
410
|
-
|
|
411
|
-
// Timer control
|
|
412
|
-
pauseTimer(): void
|
|
413
|
-
resumeTimer(): void
|
|
414
|
-
isTimerPaused(): boolean
|
|
415
|
-
|
|
416
|
-
// Cleanup
|
|
417
|
-
destroy(): void
|
|
418
|
-
}
|
|
419
|
-
```
|
|
465
|
+
**Authentication**: AWS via OIDC (no access keys), npm via Trusted Publishing with cryptographic provenance (no tokens).
|
|
420
466
|
|
|
421
|
-
|
|
467
|
+
## Secure Publishing & Provenance
|
|
422
468
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
userId: string;
|
|
428
|
-
orgId?: string;
|
|
429
|
-
email?: string;
|
|
430
|
-
startTime: number;
|
|
431
|
-
expiresAt: number;
|
|
432
|
-
durationMinutes: number;
|
|
433
|
-
jwt: string;
|
|
434
|
-
}
|
|
469
|
+
- **Dual Publishing**: [npm](https://www.npmjs.com/package/@mission_sciences/provider-sdk) (public) + AWS CodeArtifact (private)
|
|
470
|
+
- **Cryptographic Signatures**: All releases signed with GitHub Actions OIDC
|
|
471
|
+
- **Provenance Transparency**: Build provenance in [Sigstore transparency log](https://search.sigstore.dev)
|
|
472
|
+
- **No Hardcoded Secrets**: CI/CD uses OIDC for all authentication
|
|
435
473
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
reason: 'expired' | 'manual' | 'error';
|
|
440
|
-
actualDurationMinutes: number;
|
|
441
|
-
}
|
|
474
|
+
```bash
|
|
475
|
+
# Verify provenance
|
|
476
|
+
npm view @mission_sciences/provider-sdk --json | jq .dist
|
|
442
477
|
```
|
|
443
478
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
## 🚀 Production Deployment
|
|
479
|
+
## Production Checklist
|
|
447
480
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
- [ ] Update `jwksUrl` to production endpoint
|
|
481
|
+
- [ ] Set `jwksUri` to production JWKS endpoint
|
|
451
482
|
- [ ] Set correct `applicationId`
|
|
452
|
-
- [ ]
|
|
453
|
-
- [ ]
|
|
454
|
-
- [ ]
|
|
455
|
-
- [ ]
|
|
456
|
-
- [ ]
|
|
457
|
-
- [ ]
|
|
458
|
-
- [ ]
|
|
459
|
-
|
|
460
|
-
See [INTEGRATION_GUIDE.md#production-deployment](./INTEGRATION_GUIDE.md#production-deployment) for complete checklist.
|
|
483
|
+
- [ ] Set `hookTimeoutMs` appropriately for your auth provider latency
|
|
484
|
+
- [ ] Enable HTTPS on all endpoints
|
|
485
|
+
- [ ] Configure CORS headers
|
|
486
|
+
- [ ] Set up secrets management for backend token exchange
|
|
487
|
+
- [ ] Enable rate limiting on auth endpoints
|
|
488
|
+
- [ ] Verify tokens server-side (not just client-side JWKS)
|
|
489
|
+
- [ ] Test with production JWTs
|
|
461
490
|
|
|
462
|
-
##
|
|
491
|
+
## Troubleshooting
|
|
463
492
|
|
|
464
|
-
|
|
493
|
+
**"No token found in URL parameter 'gwSession' or storage"** -- The SDK looks for the JWT in the URL query parameter configured via `jwtParamName` (default: `gwSession`). Make sure the marketplace redirect includes the JWT as `?gwSession=<token>` (or your custom parameter name).
|
|
465
494
|
|
|
466
|
-
**JWT validation failed**
|
|
467
|
-
- Check JWKS URL is correct
|
|
468
|
-
- Verify applicationId matches
|
|
469
|
-
- Ensure JWT not expired
|
|
495
|
+
**JWT validation failed** -- Check that `jwksUri` points to the correct JWKS endpoint and that the JWT hasn't expired.
|
|
470
496
|
|
|
471
|
-
**
|
|
472
|
-
- Verify mount element exists
|
|
473
|
-
- Check SDK initialized
|
|
474
|
-
- Confirm active session
|
|
497
|
+
**Hook timeout** -- The default `hookTimeoutMs` is 5000ms. If your auth exchange involves multiple API calls (user lookup + creation + token grant), increase it to 10000ms or more.
|
|
475
498
|
|
|
476
|
-
**
|
|
477
|
-
- Check Cognito configuration
|
|
478
|
-
- Verify client secret (if required)
|
|
479
|
-
- Review server logs
|
|
499
|
+
**Session header not rendering** -- `SessionHeader` is a separate exported class, not a method on `MarketplaceSDK`. Import and instantiate it directly.
|
|
480
500
|
|
|
481
|
-
|
|
501
|
+
**Multi-tab conflicts** -- Enable `enableTabSync: true` to elect a master tab and sync session state across tabs via BroadcastChannel.
|
|
482
502
|
|
|
483
|
-
##
|
|
484
|
-
|
|
485
|
-
- **[Integration Guide](./INTEGRATION_GUIDE.md)** - Complete integration reference
|
|
486
|
-
- **[Quick Start](./QUICKSTART.md)** - Get started in 3 minutes
|
|
487
|
-
- **[Testing Guide](./TESTING_GUIDE.md)** - Testing strategies
|
|
488
|
-
- **[JWT Spec](./jwt-specification.md)** - Token format details
|
|
489
|
-
- **[Examples](./examples/)** - Sample implementations
|
|
490
|
-
- **[GhostDog Integration](../extension-ghostdog/MARKETPLACE_INTEGRATION.md)** - Real-world example
|
|
491
|
-
|
|
492
|
-
## 📦 Migration from @marketplace/provider-sdk
|
|
493
|
-
|
|
494
|
-
### Repository Migration
|
|
495
|
-
|
|
496
|
-
This package has been migrated from Bitbucket to GitHub with enhanced security and public availability:
|
|
497
|
-
|
|
498
|
-
**Old:**
|
|
499
|
-
- Repository: Bitbucket (private)
|
|
500
|
-
- Package: `@marketplace/provider-sdk`
|
|
501
|
-
- Registry: AWS CodeArtifact only (private)
|
|
502
|
-
- CI/CD: Bitbucket Pipelines with hardcoded credentials
|
|
503
|
-
|
|
504
|
-
**New:**
|
|
505
|
-
- Repository: [GitHub/Mission-Sciences/provider-sdk](https://github.com/Mission-Sciences/provider-sdk) (public)
|
|
506
|
-
- Package: `@mission_sciences/provider-sdk`
|
|
507
|
-
- Registry: npm (public) + AWS CodeArtifact (private)
|
|
508
|
-
- CI/CD: GitHub Actions with OIDC (zero secrets)
|
|
509
|
-
- Security: Cryptographic provenance attestation
|
|
510
|
-
|
|
511
|
-
### Migration Steps
|
|
512
|
-
|
|
513
|
-
#### Step 1: Update package.json
|
|
503
|
+
## Migration from @marketplace/provider-sdk
|
|
514
504
|
|
|
515
505
|
```bash
|
|
506
|
+
# 1. Update package
|
|
516
507
|
npm uninstall @marketplace/provider-sdk
|
|
517
508
|
npm install @mission_sciences/provider-sdk
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
#### Step 2: Update imports
|
|
521
509
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
import MarketplaceSDK from '@
|
|
525
|
-
|
|
526
|
-
// New
|
|
527
|
-
import MarketplaceSDK from '@mission_sciences/provider-sdk';
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
#### Step 3: Simplify registry config
|
|
531
|
-
|
|
532
|
-
**If using npm (public registry):**
|
|
533
|
-
```bash
|
|
534
|
-
# Remove .npmrc - use default npm registry (no configuration needed!)
|
|
510
|
+
# 2. Update imports
|
|
511
|
+
# Old: import MarketplaceSDK from '@marketplace/provider-sdk';
|
|
512
|
+
# New: import { MarketplaceSDK } from '@mission_sciences/provider-sdk';
|
|
535
513
|
```
|
|
536
514
|
|
|
537
|
-
|
|
538
|
-
```bash
|
|
539
|
-
# Update your .npmrc
|
|
540
|
-
@mission_sciences:registry=https://general-wisdom-dev-540845145946.d.codeartifact.us-east-1.amazonaws.com/npm/sdk-packages/
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
**Note**: The API is 100% compatible. No code changes required beyond the package name!
|
|
544
|
-
|
|
545
|
-
### Benefits of Migration
|
|
515
|
+
The API is 100% compatible. No code changes required beyond the package name.
|
|
546
516
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
517
|
+
**What changed:**
|
|
518
|
+
- Repository: Bitbucket (private) -> [GitHub](https://github.com/Mission-Sciences/provider-sdk) (public)
|
|
519
|
+
- Package: `@marketplace/provider-sdk` -> `@mission_sciences/provider-sdk`
|
|
520
|
+
- Registry: CodeArtifact only -> npm (public) + CodeArtifact (private)
|
|
521
|
+
- CI/CD: Bitbucket Pipelines -> GitHub Actions with OIDC
|
|
522
|
+
- Security: Added cryptographic provenance attestation
|
|
552
523
|
|
|
553
|
-
##
|
|
524
|
+
## Changelog
|
|
554
525
|
|
|
555
|
-
|
|
526
|
+
### v0.1.2 (2025-01-11) -- Migration Release
|
|
527
|
+
- Migrated from Bitbucket to GitHub
|
|
528
|
+
- Package renamed: `@marketplace/provider-sdk` -> `@mission_sciences/provider-sdk`
|
|
529
|
+
- Added cryptographic provenance attestation
|
|
530
|
+
- Dual publishing: npm (public) + AWS CodeArtifact (private)
|
|
531
|
+
- Zero-secret CI/CD with OIDC authentication
|
|
532
|
+
- Added lifecycle hooks (`onSessionStart`, `onSessionEnd`, `onSessionWarning`, `onSessionExtend`)
|
|
533
|
+
- Added heartbeat, multi-tab sync, session extension, early completion
|
|
534
|
+
- Added auth integration demo with 5 identity providers
|
|
556
535
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
MIT License - see [LICENSE](./LICENSE) file for details
|
|
560
|
-
|
|
561
|
-
## 🆘 Support
|
|
562
|
-
|
|
563
|
-
- **Issues**: [GitHub Issues](https://github.com/Mission-Sciences/provider-sdk/issues)
|
|
564
|
-
- **Email**: support@generalwisdom.com
|
|
565
|
-
- **Docs**: [docs.generalwisdom.com](https://docs.generalwisdom.com)
|
|
566
|
-
|
|
567
|
-
## 📊 Changelog
|
|
568
|
-
|
|
569
|
-
### v0.1.2 (2025-01-11) - Migration Release
|
|
570
|
-
- 🏗️ Migrated from Bitbucket to GitHub
|
|
571
|
-
- 📦 Package renamed: `@marketplace/provider-sdk` → `@mission_sciences/provider-sdk`
|
|
572
|
-
- 🔒 Added cryptographic provenance attestation
|
|
573
|
-
- ☁️ Dual publishing: npm (public) + AWS CodeArtifact (private)
|
|
574
|
-
- 🔐 Zero-secret CI/CD with OIDC authentication
|
|
575
|
-
- 📝 Comprehensive migration documentation
|
|
576
|
-
- 🚀 GitHub Actions workflow with 8-job pipeline
|
|
577
|
-
|
|
578
|
-
### v0.1.1 (2024) - Pre-Migration
|
|
536
|
+
### v0.1.1 (2024) -- Pre-Migration
|
|
579
537
|
- Initial Bitbucket release
|
|
580
538
|
- CodeArtifact-only distribution
|
|
581
|
-
- Bitbucket Pipelines CI/CD
|
|
582
539
|
|
|
583
|
-
|
|
584
|
-
- Heartbeat system
|
|
585
|
-
- Multi-tab coordination
|
|
586
|
-
- Session extension
|
|
587
|
-
- Early completion
|
|
588
|
-
- Visibility API integration
|
|
540
|
+
## License
|
|
589
541
|
|
|
590
|
-
|
|
591
|
-
- JWT validation with JWKS
|
|
592
|
-
- Session timer management
|
|
593
|
-
- Lifecycle hooks
|
|
594
|
-
- Session header component
|
|
542
|
+
MIT -- see [LICENSE](./LICENSE)
|
|
595
543
|
|
|
596
|
-
|
|
544
|
+
## Support
|
|
597
545
|
|
|
598
|
-
**
|
|
546
|
+
- **Issues**: [GitHub Issues](https://github.com/Mission-Sciences/provider-sdk/issues)
|
|
547
|
+
- **Email**: support@generalwisdom.com
|