@syndew/oluso-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,510 @@
1
- # Temporary Holding Version
1
+ # @syndew/oluso-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ JavaScript/TypeScript SDK for Oluso. Includes journey forms (waitlists, surveys), authentication (inline/modal journeys and OIDC redirect), role/permission checks, and drop-in embed scripts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @syndew/oluso-sdk
9
+ ```
10
+
11
+ ## Packages
12
+
13
+ | Entry Point | Description |
14
+ |-------------|-------------|
15
+ | `@syndew/oluso-sdk` | Core client + auth SDK (ESM/CJS) |
16
+ | `@syndew/oluso-sdk/react` | React components and hooks |
17
+ | `@syndew/oluso-sdk/embed` | Drop-in `<script>` for journey modals |
18
+ | `@syndew/oluso-sdk/auth` | Auth SDK standalone (ESM/CJS) |
19
+ | `@syndew/oluso-sdk/auth-embed` | Drop-in `<script>` for auth modals |
20
+
21
+ ---
22
+
23
+ ## Journey Forms
24
+
25
+ ### Vanilla JavaScript
26
+
27
+ ```typescript
28
+ import { createClient } from '@syndew/oluso-sdk';
29
+
30
+ const oluso = createClient({
31
+ serverUrl: 'https://auth.yoursite.com',
32
+ tenant: 'acme',
33
+ apiKey: 'pk_...', // Optional
34
+ });
35
+
36
+ // Get journey info (form fields)
37
+ const journey = await oluso.getJourney('waitlist');
38
+ console.log(journey.fields); // [{ name: 'email', type: 'email', ... }]
39
+
40
+ // Submit data
41
+ const result = await oluso.submit('waitlist', {
42
+ email: 'user@example.com',
43
+ name: 'John Doe',
44
+ });
45
+
46
+ if (result.success) {
47
+ console.log('Submitted!', result.submissionId);
48
+ }
49
+ ```
50
+
51
+ ### React
52
+
53
+ ```tsx
54
+ import { OlusoProvider, JourneyForm, useJourney } from '@syndew/oluso-sdk/react';
55
+
56
+ function App() {
57
+ return (
58
+ <OlusoProvider config={{ serverUrl: 'https://auth.yoursite.com', tenant: 'acme' }}>
59
+ <WaitlistPage />
60
+ </OlusoProvider>
61
+ );
62
+ }
63
+
64
+ // Pre-built form component
65
+ function WaitlistPage() {
66
+ return (
67
+ <JourneyForm
68
+ slug="waitlist"
69
+ onSuccess={() => console.log('Thanks!')}
70
+ submitText="Join Waitlist"
71
+ />
72
+ );
73
+ }
74
+
75
+ // Headless hook for custom UI
76
+ function CustomForm() {
77
+ const { journey, values, setValue, submit, isSubmitting, fieldErrors } = useJourney({
78
+ slug: 'waitlist',
79
+ onSuccess: (result) => console.log('Submitted!', result),
80
+ });
81
+
82
+ if (!journey) return <div>Loading...</div>;
83
+
84
+ return (
85
+ <form onSubmit={(e) => { e.preventDefault(); submit(); }}>
86
+ {journey.fields.map((field) => (
87
+ <div key={field.name}>
88
+ <label>{field.label}</label>
89
+ <input
90
+ type={field.type}
91
+ value={values[field.name] as string || ''}
92
+ onChange={(e) => setValue(field.name, e.target.value)}
93
+ required={field.required}
94
+ />
95
+ {fieldErrors[field.name] && <span>{fieldErrors[field.name]}</span>}
96
+ </div>
97
+ ))}
98
+ <button type="submit" disabled={isSubmitting}>Submit</button>
99
+ </form>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ### Multi-Step Journeys
105
+
106
+ Any journey can run step by step from your own UI. Each call returns the session: the
107
+ step to render (`session.step`, with its type and model) or the completion.
108
+
109
+ ```typescript
110
+ const oluso = createClient({ serverUrl: 'https://auth.yoursite.com', tenant: 'acme' });
111
+
112
+ let session = await oluso.startJourney('onboarding-survey');
113
+ while (session.status === 'in_progress') {
114
+ const input = await renderStep(session.step); // your UI for session.step
115
+ session = await oluso.submitStep(session.sessionId, input);
116
+ }
117
+
118
+ if (session.completed) {
119
+ console.log(session.message);
120
+ }
121
+ ```
122
+
123
+ `oluso.getSession(sessionId)` returns where a session stands without running it.
124
+ Authentication journeys run inline through the auth client (`auth.signIn(data)`), which
125
+ completes them with tokens.
126
+
127
+ ### Drop-in Embed (No Build Step)
128
+
129
+ Add the script to any HTML page. Buttons with `data-oluso-trigger` open a modal form:
130
+
131
+ ```html
132
+ <script src="https://cdn.oluso.io/sdk/embed.js"></script>
133
+
134
+ <button data-oluso-trigger="waitlist">Join Waitlist</button>
135
+ <button data-oluso-trigger="demo">Request Demo</button>
136
+
137
+ <script>
138
+ Oluso.init({
139
+ serverUrl: 'https://auth.yoursite.com',
140
+ tenant: 'acme',
141
+ journeys: {
142
+ waitlist: { title: 'Join the Waitlist' },
143
+ demo: { title: 'Request a Demo' },
144
+ },
145
+ });
146
+ </script>
147
+ ```
148
+
149
+ Programmatic API:
150
+
151
+ ```javascript
152
+ Oluso.open('waitlist');
153
+ Oluso.close();
154
+ ```
155
+
156
+ ### Embedding a journey in your own iframe
157
+
158
+ Journeys can ask for documents and photos (a file field with *Take a photo*, the
159
+ Business Verification step's supporting documents, or a verification provider's widget
160
+ that captures ID or a selfie). Browsers only give an iframe the camera and microphone
161
+ when the host page allows it, so include `allow="camera; microphone"`:
162
+
163
+ ```html
164
+ <iframe
165
+ src="https://auth.yoursite.com/journey/start/merchant-signup"
166
+ allow="camera; microphone"
167
+ style="border:none;width:100%;min-height:640px"
168
+ title="Merchant sign-up"></iframe>
169
+ ```
170
+
171
+ `JourneyEmbed` (React) and the modal sign-in (`auth.signIn()`) set this for you. If your
172
+ site sends a `Permissions-Policy` header, it must also allow `camera` and `microphone` for
173
+ the auth server's origin (e.g. `Permissions-Policy: camera=(self "https://auth.yoursite.com")`).
174
+
175
+ ---
176
+
177
+ ## Authentication
178
+
179
+ The auth SDK provides inline and modal authentication (using the journey system) and standard OIDC redirect flow, plus role and permission checking.
180
+
181
+ ### Setup
182
+
183
+ ```typescript
184
+ import { OlusoAuth } from '@syndew/oluso-sdk';
185
+
186
+ const auth = new OlusoAuth({
187
+ serverUrl: 'https://auth.yoursite.com',
188
+ tenant: 'acme',
189
+ clientId: 'my-spa-client',
190
+ storage: 'localStorage', // 'memory' | 'localStorage' | 'sessionStorage'
191
+ onAuthStateChange: (user) => {
192
+ console.log('Auth state:', user);
193
+ },
194
+ });
195
+ ```
196
+
197
+ ### Modal Sign-In (Secure Iframe)
198
+
199
+ Call `signIn()` without arguments to open a secure modal. Credentials are entered inside an iframe hosted on the auth server — the host page never has access to passwords:
200
+
201
+ ```typescript
202
+ // Opens modal, resolves when auth completes
203
+ const result = await auth.signIn();
204
+
205
+ if (result.success) {
206
+ console.log('Logged in:', result.user.name);
207
+ }
208
+ ```
209
+
210
+ This is the recommended approach for most integrations. It works on any site (first-party or third-party) and handles MFA, password reset, etc. inside the iframe automatically.
211
+
212
+ ### Programmatic Sign-In (First-Party Only)
213
+
214
+ For trusted first-party apps where you control the page and collect credentials yourself.
215
+ The journey runs step by step through the headless journey API and completes with an
216
+ authorization code bound to this client's PKCE verifier, which the SDK redeems for tokens:
217
+
218
+ ```typescript
219
+ const result = await auth.signIn({
220
+ username: 'user@example.com',
221
+ password: 'secret',
222
+ });
223
+
224
+ if (result.success) {
225
+ console.log('Logged in:', result.user.name);
226
+ console.log('Token:', result.tokens.accessToken);
227
+ } else if (result.requiresStep) {
228
+ // MFA or another step: result.step describes what to render
229
+ const mfaResult = await auth.continueStep(result.sessionId, {
230
+ code: '123456',
231
+ });
232
+ } else {
233
+ console.error(result.error); // e.g. invalid credentials
234
+ }
235
+ ```
236
+
237
+ The calling page's origin must be registered for the client (one of its CORS origins, or the
238
+ origin of one of its redirect URIs); a journey's own allowed origins can narrow that further. The
239
+ client must be enabled and allow the authorization code grant, the scopes must be ones the client
240
+ is allowed, and a PKCE S256 challenge is required (the SDK sends one). The same rules apply to
241
+ every sign-in that returns a code to the page: the modal, passkeys and FedCM.
242
+
243
+ > **Security note**: Programmatic sign-in passes credentials through JavaScript on the host page. Only use this on pages you fully control. For third-party sites or untrusted environments, use the modal approach above.
244
+
245
+ ### Sign-Up
246
+
247
+ ```typescript
248
+ // Modal (secure)
249
+ const result = await auth.signUp();
250
+
251
+ // Or programmatic (first-party)
252
+ const result = await auth.signUp({
253
+ name: 'Jane Doe',
254
+ email: 'jane@example.com',
255
+ password: 'securePassword123',
256
+ });
257
+ ```
258
+
259
+ ### OIDC Redirect Flow
260
+
261
+ For apps that prefer the standard redirect-based flow:
262
+
263
+ ```typescript
264
+ // Redirect to login page
265
+ await auth.loginWithRedirect({
266
+ prompt: 'login', // Optional: 'login' | 'consent' | 'none'
267
+ state: '/dashboard', // Optional: return path
268
+ });
269
+
270
+ // On callback page (/callback)
271
+ const result = await auth.handleCallback();
272
+ if (result.success) {
273
+ window.location.href = '/dashboard';
274
+ }
275
+ ```
276
+
277
+ ### Checking Roles & Permissions
278
+
279
+ ```typescript
280
+ const user = auth.getUser();
281
+
282
+ // Single role check
283
+ if (auth.hasRole('admin')) {
284
+ showAdminPanel();
285
+ }
286
+
287
+ // Any of multiple roles
288
+ if (auth.hasAnyRole('admin', 'editor', 'moderator')) {
289
+ showEditButton();
290
+ }
291
+
292
+ // Permission check
293
+ if (auth.hasPermission('documents:write')) {
294
+ enableUpload();
295
+ }
296
+
297
+ // Access user claims directly
298
+ console.log(user.email);
299
+ console.log(user.tenantId);
300
+ console.log(user.claims); // Raw ID token claims
301
+ ```
302
+
303
+ ### Token Management
304
+
305
+ ```typescript
306
+ // Get access token for API calls
307
+ const token = auth.getAccessToken();
308
+ fetch('/api/data', {
309
+ headers: { Authorization: `Bearer ${token}` },
310
+ });
311
+
312
+ // Refresh token
313
+ const refreshed = await auth.refreshToken();
314
+
315
+ // Check auth state
316
+ if (auth.isAuthenticated()) { ... }
317
+
318
+ // Fetch updated user info from server
319
+ const user = await auth.fetchUserInfo();
320
+ ```
321
+
322
+ ### Logout
323
+
324
+ ```typescript
325
+ // Local logout (clear tokens)
326
+ auth.logout();
327
+
328
+ // Logout with server-side session end
329
+ auth.logout({ redirect: true });
330
+
331
+ // Custom post-logout redirect
332
+ auth.logout({
333
+ redirect: true,
334
+ postLogoutRedirectUri: 'https://yoursite.com/goodbye',
335
+ });
336
+ ```
337
+
338
+ ### Passkeys (WebAuthn)
339
+
340
+ Sign in with passkeys — no form, no iframe, just a native biometric/key prompt. Works with discoverable credentials (usernameless) and username-based flows.
341
+
342
+ ```typescript
343
+ // Discoverable credential (shows all passkeys for this site)
344
+ const result = await auth.signInWithPasskey();
345
+
346
+ // Username-based (shows passkeys for a specific user)
347
+ const result = await auth.signInWithPasskey({ username: 'user@example.com' });
348
+
349
+ // Conditional mediation (passkey suggestions in autofill)
350
+ const ac = new AbortController();
351
+ const result = await auth.signInWithPasskey({ conditional: true, signal: ac.signal });
352
+
353
+ if (result.success) {
354
+ console.log('Signed in with passkey:', result.user.name);
355
+ }
356
+ ```
357
+
358
+ > **Note**: Passkeys require the FIDO2 plugin to be enabled on the server (and passkeys not turned off for the tenant). Users must have previously registered a passkey via their account settings. The page's origin must be registered for the client and the scopes allowed for it, as for programmatic sign-in above.
359
+
360
+ ### FedCM (Browser-Native Account Picker)
361
+
362
+ FedCM enables browser-native federated sign-in without redirects or popups. The browser shows an account picker if the user has an active session on the auth server.
363
+
364
+ ```typescript
365
+ const result = await auth.signInWithFedCM();
366
+
367
+ // With login hint to pre-select an account
368
+ const result = await auth.signInWithFedCM({ loginHint: 'user@example.com' });
369
+
370
+ // Silent re-authentication (if user previously approved)
371
+ const result = await auth.signInWithFedCM({ autoReauth: true });
372
+
373
+ if (result.success) {
374
+ console.log('Signed in via FedCM:', result.user.name);
375
+ }
376
+ ```
377
+
378
+ > **Browser support**: Chrome 132+, Edge 132+ (the SDK binds the code to the page with a PKCE challenge sent in the provider's `params`). Returns an error on unsupported browsers — use as a progressive enhancement alongside modal sign-in. The page's origin must be registered for the client.
379
+
380
+ ### OIDC Endpoint Discovery
381
+
382
+ Endpoints are automatically discovered from `.well-known/openid-configuration`. You can also specify them manually:
383
+
384
+ ```typescript
385
+ const auth = new OlusoAuth({
386
+ serverUrl: 'https://auth.yoursite.com',
387
+ tenant: 'acme',
388
+ clientId: 'my-spa-client',
389
+ endpoints: {
390
+ authorization: 'https://auth.yoursite.com/connect/authorize',
391
+ token: 'https://auth.yoursite.com/connect/token',
392
+ userinfo: 'https://auth.yoursite.com/connect/userinfo',
393
+ endSession: 'https://auth.yoursite.com/connect/endsession',
394
+ },
395
+ });
396
+ ```
397
+
398
+ ### Drop-in Auth Embed (No Build Step)
399
+
400
+ Add auth to any site with a single script tag:
401
+
402
+ ```html
403
+ <script src="https://cdn.oluso.io/sdk/auth-embed.js"></script>
404
+
405
+ <button data-oluso-auth="signin">Sign In</button>
406
+ <button data-oluso-auth="signup">Create Account</button>
407
+ <button data-oluso-auth="passkey">Sign In with Passkey</button>
408
+
409
+ <script>
410
+ OlusoAuth.init({
411
+ serverUrl: 'https://auth.yoursite.com',
412
+ tenant: 'acme',
413
+ clientId: 'my-spa-client',
414
+ storage: 'localStorage',
415
+ });
416
+
417
+ // Listen for auth state changes
418
+ OlusoAuth.onAuth((user) => {
419
+ if (user) {
420
+ document.getElementById('greeting').textContent = `Hello, ${user.name}`;
421
+ }
422
+ });
423
+ </script>
424
+ ```
425
+
426
+ Programmatic API:
427
+
428
+ ```javascript
429
+ OlusoAuth.open('signin'); // Open sign-in modal
430
+ OlusoAuth.open('signup'); // Open sign-up modal
431
+ OlusoAuth.close();
432
+
433
+ // Passkey & FedCM (no modal needed)
434
+ OlusoAuth.signInWithPasskey(); // Native passkey prompt
435
+ OlusoAuth.signInWithFedCM(); // Browser account picker
436
+
437
+ // After auth
438
+ OlusoAuth.getUser(); // { sub, name, email, roles, permissions, ... }
439
+ OlusoAuth.hasRole('admin'); // true/false
440
+ OlusoAuth.hasPermission('read'); // true/false
441
+ OlusoAuth.logout();
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Configuration Reference
447
+
448
+ ### `OlusoConfig` (Journey Client)
449
+
450
+ | Option | Type | Required | Description |
451
+ |--------|------|----------|-------------|
452
+ | `serverUrl` | `string` | Yes | Base URL of your Oluso server |
453
+ | `tenant` | `string` | Yes | Your tenant ID or subdomain |
454
+ | `apiKey` | `string` | No | Public API key |
455
+ | `fetch` | `typeof fetch` | No | Custom fetch implementation (SSR) |
456
+
457
+ ### `OlusoAuthConfig` (Auth Client)
458
+
459
+ | Option | Type | Required | Description |
460
+ |--------|------|----------|-------------|
461
+ | `serverUrl` | `string` | Yes | Base URL of your Oluso server |
462
+ | `tenant` | `string` | Yes | Your tenant ID or subdomain |
463
+ | `clientId` | `string` | Yes | OAuth2 client ID (public client) |
464
+ | `redirectUri` | `string` | No | Redirect URI for OIDC code flow |
465
+ | `scopes` | `string` | No | Scopes to request (default: `openid profile email`) |
466
+ | `endpoints` | `OidcEndpoints` | No | Manual OIDC endpoint overrides |
467
+ | `signInJourney` | `string` | No | Slug of the sign-in journey (default: `signin`) |
468
+ | `signUpJourney` | `string` | No | Slug of the sign-up journey (default: `signup`) |
469
+ | `passwordResetJourney` | `string` | No | Slug of the password-reset journey |
470
+ | `storage` | `string` | No | Token storage: `memory`, `localStorage`, `sessionStorage` |
471
+ | `apiKey` | `string` | No | Public API key, sent as `X-API-Key` |
472
+ | `onAuthStateChange` | `function` | No | Callback when auth state changes |
473
+
474
+ ---
475
+
476
+ ## Events
477
+
478
+ ```typescript
479
+ const oluso = createClient({ ... });
480
+
481
+ oluso.on('submission:success', (event) => {
482
+ console.log('Submitted:', event.data);
483
+ });
484
+ ```
485
+
486
+ Event types: `journey:started`, `journey:step`, `journey:completed`, `journey:error`, `submission:success`, `submission:error`, `auth:success`, `auth:error`. Each event carries the journey's `slug`; journey events also carry the `sessionId`.
487
+
488
+ ---
489
+
490
+ ## TypeScript
491
+
492
+ All types are exported:
493
+
494
+ ```typescript
495
+ import type {
496
+ OlusoConfig,
497
+ JourneyInfo,
498
+ JourneyField,
499
+ SubmissionResult,
500
+ OlusoAuthConfig,
501
+ OlusoUser,
502
+ TokenSet,
503
+ AuthResult,
504
+ OidcEndpoints,
505
+ JourneySession,
506
+ JourneyStepView,
507
+ StepResult,
508
+ JourneyStepData,
509
+ } from '@syndew/oluso-sdk';
510
+ ```