@axcode/axcloud-sdk 0.1.7-dev.580a922 → 0.1.7-dev.bf490b9

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
@@ -12,10 +12,10 @@ npm install @axcode/axcloud-sdk
12
12
 
13
13
  The SDK is published with two tags for different environments:
14
14
 
15
- | Tag | Purpose | Stability | Install |
16
- |-----|---------|-----------|---------|
17
- | `latest` | Production | Stable, tested | `npm install @axcode/axcloud-sdk` |
18
- | `next` | Development/Staging | Preview, may have breaking changes | `npm install @axcode/axcloud-sdk@next` |
15
+ | Tag | Purpose | Stability | Install |
16
+ | -------- | ------------------- | ---------------------------------- | -------------------------------------- |
17
+ | `latest` | Production | Stable, tested | `npm install @axcode/axcloud-sdk` |
18
+ | `next` | Development/Staging | Preview, may have breaking changes | `npm install @axcode/axcloud-sdk@next` |
19
19
 
20
20
  **Recommended setup:**
21
21
 
@@ -42,46 +42,51 @@ This allows you to test new SDK features in your staging environment before they
42
42
  ### 1. Wrap your app with AxCloudProvider
43
43
 
44
44
  ```tsx
45
- import { AxCloudProvider } from '@axcode/axcloud-sdk'
45
+ import { AxCloudProvider } from '@axcode/axcloud-sdk';
46
46
 
47
47
  function App() {
48
48
  return (
49
49
  <AxCloudProvider
50
- orgId="org_ba81807b" // Your organization ID
50
+ orgId="org_ba81807b" // Your organization ID
51
51
  apiUrl="https://api.axcloud.app"
52
- apiKey="pk_live_..." // Public API key from Settings → API Keys
52
+ apiKey="pk_live_..." // Public API key from Settings → API Keys
53
53
  auth0={{
54
- domain: "auth.axcloud.app",
55
- clientId: "your-client-id",
56
- redirectUri: window.location.origin
54
+ domain: 'auth.axcloud.app',
55
+ clientId: 'your-client-id',
56
+ redirectUri: window.location.origin,
57
+ organization: 'org_auth0_...', // Optional Auth0 Organization ID for invite-only member sites
57
58
  }}
58
59
  >
59
60
  <Router>
60
61
  <Routes />
61
62
  </Router>
62
63
  </AxCloudProvider>
63
- )
64
+ );
64
65
  }
65
66
  ```
66
67
 
68
+ `orgId` is the AxCloud organization ID used by CMS APIs, for example `org_ba81807b`.
69
+ `auth0.organization` is the Auth0 Organization ID used during login, for example `org_o960yILn77d4vY6u`.
70
+ Set `auth0.organization` for invite-only customer member sites so Universal Login is scoped to that customer organization.
71
+
67
72
  ### 2. Fetch content
68
73
 
69
74
  ```tsx
70
- import { useNews } from '@axcode/axcloud-sdk'
75
+ import { useNews } from '@axcode/axcloud-sdk';
71
76
 
72
77
  function NewsPage() {
73
- const { data: news, isLoading, isError } = useNews()
78
+ const { data: news, isLoading, isError } = useNews();
74
79
 
75
- if (isLoading) return <Spinner />
76
- if (isError) return <Error />
80
+ if (isLoading) return <Spinner />;
81
+ if (isError) return <Error />;
77
82
 
78
83
  return (
79
84
  <ul>
80
- {news.map(item => (
85
+ {news.map((item) => (
81
86
  <li key={item.id}>{item.title}</li>
82
87
  ))}
83
88
  </ul>
84
- )
89
+ );
85
90
  }
86
91
  ```
87
92
 
@@ -90,31 +95,31 @@ function NewsPage() {
90
95
  Pages and news can be stored once in AxCloud with translations and requested per language:
91
96
 
92
97
  ```tsx
93
- import { useContentItem, useNews } from '@axcode/axcloud-sdk'
98
+ import { useContentItem, useNews } from '@axcode/axcloud-sdk';
94
99
 
95
100
  function AboutPage({ locale }: { locale: 'sv' | 'en' }) {
96
101
  const { data: page } = useContentItem('pages', 'about-us', {
97
102
  lang: locale,
98
103
  publicOnly: true,
99
- })
104
+ });
100
105
 
101
- return <h1>{page?.title}</h1>
106
+ return <h1>{page?.title}</h1>;
102
107
  }
103
108
 
104
109
  function NewsList({ locale }: { locale: 'sv' | 'en' }) {
105
- const { data: news } = useNews({ lang: locale })
110
+ const { data: news } = useNews({ lang: locale });
106
111
 
107
- return news.map(item => <article key={item.id}>{item.title}</article>)
112
+ return news.map((item) => <article key={item.id}>{item.title}</article>);
108
113
  }
109
114
  ```
110
115
 
111
116
  ### 3. Add authentication
112
117
 
113
118
  ```tsx
114
- import { useAuth, LoginButton, LogoutButton, ProtectedRoute } from '@axcode/axcloud-sdk'
119
+ import { useAuth, LoginButton, LogoutButton, ProtectedRoute } from '@axcode/axcloud-sdk';
115
120
 
116
121
  function Header() {
117
- const { isAuthenticated, user } = useAuth()
122
+ const { isAuthenticated, user } = useAuth();
118
123
 
119
124
  return (
120
125
  <header>
@@ -127,7 +132,7 @@ function Header() {
127
132
  <LoginButton>Sign In</LoginButton>
128
133
  )}
129
134
  </header>
130
- )
135
+ );
131
136
  }
132
137
 
133
138
  function MemberArea() {
@@ -135,87 +140,158 @@ function MemberArea() {
135
140
  <ProtectedRoute>
136
141
  <MemberContent />
137
142
  </ProtectedRoute>
138
- )
143
+ );
139
144
  }
140
145
  ```
141
146
 
147
+ The SDK keeps verification UI inside the product site. Render your own branded
148
+ notice when `needsEmailVerification` is true:
149
+
150
+ ```tsx
151
+ const { needsEmailVerification, requestEmailVerification, refreshUser } = useAuth();
152
+
153
+ if (needsEmailVerification) {
154
+ // Render the product's own verification notice.
155
+ }
156
+
157
+ await requestEmailVerification({ returnTo: window.location.href });
158
+ await refreshUser();
159
+ ```
160
+
161
+ The API resolves the recipient and organization from the authenticated token.
162
+ Return URLs are restricted to the organization's configured member site.
163
+
164
+ ### 3b. Add privacy-safe analytics
165
+
166
+ Analytics is optional and provider-independent. AxCloud resolves the public
167
+ tracker configuration from the organization, site key, deployment stage, and
168
+ current hostname. SDK consumers never receive the Umami API key.
169
+
170
+ ```tsx
171
+ import { AxCloudProvider, useAnalytics } from '@axcode/axcloud-sdk';
172
+
173
+ <AxCloudProvider
174
+ orgId="org_ba81807b"
175
+ apiUrl="https://api.axcloud.app"
176
+ stage="prod"
177
+ analytics={{
178
+ enabled: analyticsConsent === 'accepted',
179
+ siteKey: 'member-site',
180
+ normalizePath: (path) => path.replace(/^\/member\/news\/[^/]+/, '/member/news/:id'),
181
+ }}
182
+ auth0={auth0Config}
183
+ >
184
+ <App />
185
+ </AxCloudProvider>;
186
+ ```
187
+
188
+ The SDK tracks SPA page changes after the tracker is ready. Query strings and
189
+ fragments are removed, common opaque IDs are replaced with `:id`, Do Not Track
190
+ is honored, and localhost collection is disabled unless `trackLocalhost: true`
191
+ is explicitly set.
192
+
193
+ Custom events must be allowlisted in the AxCloud analytics site configuration:
194
+
195
+ ```tsx
196
+ function NewsArticle() {
197
+ const analytics = useAnalytics();
198
+
199
+ const handleOpen = () => {
200
+ analytics.track('news_opened', { surface: 'member_news' });
201
+ };
202
+ }
203
+ ```
204
+
205
+ Only send coarse enums, booleans, and counts. The SDK rejects sensitive
206
+ property names and email-like values, but applications remain responsible for
207
+ never supplying names, email addresses, user IDs, document titles, search text,
208
+ chat text, or other personal/content data. Production trackers should only be
209
+ enabled after the product's privacy notice and Content Security Policy have
210
+ been reviewed.
211
+
142
212
  ### 4. Use sections
143
213
 
144
214
  ```tsx
145
- import { useMySection } from '@axcode/axcloud-sdk'
215
+ import { useMySection } from '@axcode/axcloud-sdk';
146
216
 
147
217
  function MySectionPage() {
148
- const { section, members, isLoading } = useMySection()
218
+ const { section, members, isLoading } = useMySection();
149
219
 
150
- if (isLoading) return <Spinner />
151
- if (!section) return <p>You are not assigned to a section</p>
220
+ if (isLoading) return <Spinner />;
221
+ if (!section) return <p>You are not assigned to a section</p>;
152
222
 
153
223
  return (
154
224
  <div>
155
225
  <h1>{section.name}</h1>
156
226
  <h2>Members ({members.length})</h2>
157
227
  <ul>
158
- {members.map(m => <li key={m.id}>{m.name}</li>)}
228
+ {members.map((m) => (
229
+ <li key={m.id}>{m.name}</li>
230
+ ))}
159
231
  </ul>
160
232
  </div>
161
- )
233
+ );
162
234
  }
163
235
  ```
164
236
 
165
237
  ### 4b. Filter content by section
166
238
 
167
239
  ```tsx
168
- import { useNews, useMySection } from '@axcode/axcloud-sdk'
240
+ import { useNews, useMySection } from '@axcode/axcloud-sdk';
169
241
 
170
242
  function SectionNews() {
171
- const { sectionId } = useMySection()
243
+ const { sectionId } = useMySection();
172
244
 
173
245
  // Fetch news for user's section only
174
- const { data: sectionNews } = useNews({ sectionId })
246
+ const { data: sectionNews } = useNews({ sectionId });
175
247
 
176
248
  // Or fetch all organization news
177
- const { data: allNews } = useNews()
249
+ const { data: allNews } = useNews();
178
250
 
179
251
  return (
180
252
  <div>
181
253
  <h2>Section News</h2>
182
- {sectionNews.map(item => <NewsCard key={item.id} item={item} />)}
254
+ {sectionNews.map((item) => (
255
+ <NewsCard key={item.id} item={item} />
256
+ ))}
183
257
 
184
258
  <h2>All Organization News</h2>
185
- {allNews.map(item => <NewsCard key={item.id} item={item} />)}
259
+ {allNews.map((item) => (
260
+ <NewsCard key={item.id} item={item} />
261
+ ))}
186
262
  </div>
187
- )
263
+ );
188
264
  }
189
265
  ```
190
266
 
191
267
  ### 5. Fetch organization branding and media
192
268
 
193
269
  ```tsx
194
- import { useBranding, useLogo, useMedia } from '@axcode/axcloud-sdk'
270
+ import { useBranding, useLogo, useMedia } from '@axcode/axcloud-sdk';
195
271
 
196
272
  function Header() {
197
- const { logoUrl, branding, organizationName } = useBranding()
273
+ const { logoUrl, branding, organizationName } = useBranding();
198
274
 
199
275
  return (
200
276
  <header style={{ backgroundColor: branding?.primaryColor }}>
201
277
  {logoUrl && <img src={logoUrl} alt={organizationName} />}
202
278
  <h1>{organizationName}</h1>
203
279
  </header>
204
- )
280
+ );
205
281
  }
206
282
 
207
283
  // Or just the logo
208
284
  function Logo() {
209
- const { logoUrl, isLoading } = useLogo()
285
+ const { logoUrl, isLoading } = useLogo();
210
286
 
211
- if (isLoading) return <Skeleton />
212
- return logoUrl ? <img src={logoUrl} alt="Logo" /> : <DefaultLogo />
287
+ if (isLoading) return <Skeleton />;
288
+ return logoUrl ? <img src={logoUrl} alt="Logo" /> : <DefaultLogo />;
213
289
  }
214
290
 
215
291
  // Fetch specific media file
216
292
  function HeroImage() {
217
- const { mediaUrl } = useMedia('hero-banner.jpg')
218
- return mediaUrl ? <img src={mediaUrl} alt="Hero" /> : null
293
+ const { mediaUrl } = useMedia('hero-banner.jpg');
294
+ return mediaUrl ? <img src={mediaUrl} alt="Hero" /> : null;
219
295
  }
220
296
  ```
221
297
 
@@ -224,26 +300,26 @@ function HeroImage() {
224
300
  ### 6. Fetch public documents
225
301
 
226
302
  ```tsx
227
- import { useDocuments, useDocumentDownload } from '@axcode/axcloud-sdk'
303
+ import { useDocuments, useDocumentDownload } from '@axcode/axcloud-sdk';
228
304
 
229
305
  function PublicDocuments() {
230
- const { documents, isLoading } = useDocuments({ tags: ['handbook'], limit: 20 })
306
+ const { documents, isLoading } = useDocuments({ tags: ['handbook'], limit: 20 });
231
307
 
232
- if (isLoading) return <Skeleton />
308
+ if (isLoading) return <Skeleton />;
233
309
  return (
234
310
  <ul>
235
- {documents.map(doc => (
311
+ {documents.map((doc) => (
236
312
  <li key={doc.document_id}>
237
313
  <DocumentLink documentId={doc.document_id} title={doc.title} />
238
314
  </li>
239
315
  ))}
240
316
  </ul>
241
- )
317
+ );
242
318
  }
243
319
 
244
320
  function DocumentLink({ documentId, title }: { documentId: string; title: string }) {
245
- const { documentUrl } = useDocumentDownload(documentId)
246
- return documentUrl ? <a href={documentUrl}>{title}</a> : <span>{title}</span>
321
+ const { documentUrl } = useDocumentDownload(documentId);
322
+ return documentUrl ? <a href={documentUrl}>{title}</a> : <span>{title}</span>;
247
323
  }
248
324
  ```
249
325
 
@@ -252,10 +328,10 @@ Only documents with `status: "published"` and `audience: "public"` are exposed t
252
328
  ### 7. Gate features by module
253
329
 
254
330
  ```tsx
255
- import { ModuleGate, useOrg } from '@axcode/axcloud-sdk'
331
+ import { ModuleGate, useOrg } from '@axcode/axcloud-sdk';
256
332
 
257
333
  function Dashboard() {
258
- const { hasModule } = useOrg()
334
+ const { hasModule } = useOrg();
259
335
 
260
336
  return (
261
337
  <div>
@@ -267,7 +343,7 @@ function Dashboard() {
267
343
 
268
344
  {hasModule('ai') && <AIChatWidget />}
269
345
  </div>
270
- )
346
+ );
271
347
  }
272
348
  ```
273
349
 
@@ -275,85 +351,85 @@ function Dashboard() {
275
351
 
276
352
  ### Provider
277
353
 
278
- | Component | Description |
279
- |-----------|-------------|
354
+ | Component | Description |
355
+ | ----------------- | --------------------------------------------------------------- |
280
356
  | `AxCloudProvider` | Main provider that combines Auth0, React Query, and SDK context |
281
357
 
282
358
  ### Hooks
283
359
 
284
- | Hook | Description |
285
- |------|-------------|
286
- | `useAuth()` | Login/logout with Auth0 Universal Login |
287
- | `useUser()` | Fetch current user with role and permissions |
288
- | `useOrg()` | Fetch organization info and check modules |
289
- | `useNews(options?)` | Fetch news articles. Returns public + internal + members content when authenticated. |
290
- | `useContent(type, options?)` | Fetch any content type. For `pages` and `events`, returns member content when authenticated. |
291
- | `useMySection()` | Fetch current user's section with members |
292
- | `useSections()` | Fetch all organization sections |
293
- | `useBranding()` | Fetch organization branding (logo, colors) |
294
- | `useLogo()` | Fetch organization logo URL |
295
- | `useMedia(mediaId)` | Fetch specific media file URL |
296
- | `useAvatar(userId)` | Fetch user avatar URL |
297
- | `useMediaList(type)` | List media files |
298
- | `useDocuments(options?)` | List published public documents |
299
- | `useDocumentDownload(documentId)` | Fetch a signed document download URL |
360
+ | Hook | Description |
361
+ | --------------------------------- | -------------------------------------------------------------------------------------------- |
362
+ | `useAuth()` | Login/logout, email-verification state, resend, and profile refresh |
363
+ | `useUser()` | Fetch current user with role and permissions |
364
+ | `useOrg()` | Fetch organization info and check modules |
365
+ | `useNews(options?)` | Fetch news articles. Returns public + internal + members content when authenticated. |
366
+ | `useContent(type, options?)` | Fetch any content type. For `pages` and `events`, returns member content when authenticated. |
367
+ | `useMySection()` | Fetch current user's section with members |
368
+ | `useSections()` | Fetch all organization sections |
369
+ | `useBranding()` | Fetch organization branding (logo, colors) |
370
+ | `useLogo()` | Fetch organization logo URL |
371
+ | `useMedia(mediaId)` | Fetch specific media file URL |
372
+ | `useAvatar(userId)` | Fetch user avatar URL |
373
+ | `useMediaList(type)` | List media files |
374
+ | `useDocuments(options?)` | List published public documents |
375
+ | `useDocumentDownload(documentId)` | Fetch a signed document download URL |
300
376
 
301
377
  ### Content Query Options
302
378
 
303
379
  ```typescript
304
380
  interface ContentQueryOptions {
305
- page?: number // Page number (default: 1)
306
- pageSize?: number // Items per page (default: 20)
307
- status?: 'draft' | 'published' | 'archived'
308
- visibility?: 'draft' | 'published' | 'archived'
309
- audience?: 'public' | 'internal' | 'members' | 'section'
310
- sectionId?: string // Filter by specific section
311
- search?: string // Search in title/content
312
- sortBy?: string // Sort field (default: 'createdAt')
313
- sortOrder?: 'asc' | 'desc'
314
- lang?: string // Return translated title/slug/blocks when available
315
- publicOnly?: boolean // Force public endpoint even when authenticated
381
+ page?: number; // Page number (default: 1)
382
+ pageSize?: number; // Items per page (default: 20)
383
+ status?: 'draft' | 'published' | 'archived';
384
+ visibility?: 'draft' | 'published' | 'archived';
385
+ audience?: 'public' | 'internal' | 'members' | 'section';
386
+ sectionId?: string; // Filter by specific section
387
+ search?: string; // Search in title/content
388
+ sortBy?: string; // Sort field (default: 'createdAt')
389
+ sortOrder?: 'asc' | 'desc';
390
+ lang?: string; // Return translated title/slug/blocks when available
391
+ publicOnly?: boolean; // Force public endpoint even when authenticated
316
392
  }
317
393
  ```
318
394
 
319
395
  ### Components
320
396
 
321
- | Component | Description |
322
- |-----------|-------------|
323
- | `<LoginButton>` | Trigger Auth0 login |
324
- | `<LogoutButton>` | Trigger logout |
325
- | `<ProtectedRoute>` | Require authentication to view content |
326
- | `<ModuleGate>` | Conditionally render based on enabled modules |
397
+ | Component | Description |
398
+ | ------------------ | --------------------------------------------- |
399
+ | `<LoginButton>` | Trigger Auth0 login |
400
+ | `<LogoutButton>` | Trigger logout |
401
+ | `<ProtectedRoute>` | Require authentication to view content |
402
+ | `<ModuleGate>` | Conditionally render based on enabled modules |
327
403
 
328
404
  ## Advanced Usage
329
405
 
330
406
  ### Custom API calls
331
407
 
332
408
  ```tsx
333
- import { useAuth } from '@axcode/axcloud-sdk'
409
+ import { useAuth } from '@axcode/axcloud-sdk';
334
410
 
335
411
  function MyComponent() {
336
- const { getAccessToken } = useAuth()
412
+ const { getAccessToken } = useAuth();
337
413
 
338
414
  const fetchCustomData = async () => {
339
- const token = await getAccessToken()
415
+ const token = await getAccessToken();
340
416
  const response = await fetch('/api/custom', {
341
- headers: { Authorization: `Bearer ${token}` }
342
- })
343
- return response.json()
344
- }
417
+ headers: { Authorization: `Bearer ${token}` },
418
+ });
419
+ return response.json();
420
+ };
345
421
  }
346
422
  ```
347
423
 
348
424
  ### Access the underlying Auth0 client
349
425
 
350
426
  ```tsx
351
- const { getAuth0Client } = useAuth()
352
- const auth0 = getAuth0Client()
427
+ const { getAuth0Client } = useAuth();
428
+ const auth0 = getAuth0Client();
353
429
 
354
430
  // Full Auth0 API available
355
- await auth0.getAccessTokenSilently()
356
- await auth0.getIdTokenClaims()
431
+ await auth0.getAccessTokenSilently();
432
+ await auth0.getIdTokenClaims();
357
433
  ```
358
434
 
359
435
  ## Security Model
@@ -362,58 +438,58 @@ await auth0.getIdTokenClaims()
362
438
 
363
439
  The SDK uses two types of credentials:
364
440
 
365
- 1. **API Key** (`apiKey` prop): A *public identifier* for your organization
441
+ 1. **API Key** (`apiKey` prop): A _public identifier_ for your organization
366
442
  - Safe to include in client-side code
367
443
  - Used for public content endpoints
368
444
  - Backend validates and rate-limits based on API key
369
445
  - Without a valid API key, requests return 401
370
446
 
371
- 2. **Authentication Token** (via Auth0): A *secret* JWT token
447
+ 2. **Authentication Token** (via Auth0): A _secret_ JWT token
372
448
  - Automatically managed by the SDK
373
449
  - Required for member-only content and user data
374
450
  - Never exposed in client code
375
451
 
376
452
  ### What the API Key Protects
377
453
 
378
- | Without API Key | With API Key | With Auth Token |
379
- |-----------------|--------------|-----------------|
380
- | Nothing | Public news/content | Member content |
381
- | | Organization branding | User profile |
382
- | | Public media | Linked accounts |
383
- | | | Admin functions |
454
+ | Without API Key | With API Key | With Auth Token |
455
+ | --------------- | --------------------- | --------------- |
456
+ | Nothing | Public news/content | Member content |
457
+ | | Organization branding | User profile |
458
+ | | Public media | Linked accounts |
459
+ | | | Admin functions |
384
460
 
385
461
  ### Endpoint Security
386
462
 
387
- | Endpoint Type | Authentication | Example |
388
- |--------------|---------------|---------|
389
- | Public content | API key (x-api-key header) | `/content/public/{orgId}/news` |
390
- | Member content | JWT Bearer token | `/content/members/{orgId}/news` |
391
- | User data | JWT Bearer token | `/api/users/me` |
392
- | Admin | JWT Bearer token + role | `/api/admin/forms/submissions` |
463
+ | Endpoint Type | Authentication | Example |
464
+ | -------------- | -------------------------- | ------------------------------- |
465
+ | Public content | API key (x-api-key header) | `/content/public/{orgId}/news` |
466
+ | Member content | JWT Bearer token | `/content/members/{orgId}/news` |
467
+ | User data | JWT Bearer token | `/api/users/me` |
468
+ | Admin | JWT Bearer token + role | `/api/admin/forms/submissions` |
393
469
 
394
470
  ## Error Handling
395
471
 
396
472
  ```tsx
397
- import { useNews, isAxCloudError } from '@axcode/axcloud-sdk'
473
+ import { useNews, isAxCloudError } from '@axcode/axcloud-sdk';
398
474
 
399
475
  function NewsPage() {
400
- const { data, error, isError } = useNews()
476
+ const { data, error, isError } = useNews();
401
477
 
402
478
  if (isError) {
403
479
  if (isAxCloudError(error)) {
404
480
  switch (error.code) {
405
481
  case 'UNAUTHORIZED':
406
- return <LoginPrompt />
482
+ return <LoginPrompt />;
407
483
  case 'NOT_FOUND':
408
- return <NotFound />
484
+ return <NotFound />;
409
485
  default:
410
- return <Error message={error.message} />
486
+ return <Error message={error.message} />;
411
487
  }
412
488
  }
413
- return <Error message="Something went wrong" />
489
+ return <Error message="Something went wrong" />;
414
490
  }
415
491
 
416
- return <NewsList items={data} />
492
+ return <NewsList items={data} />;
417
493
  }
418
494
  ```
419
495