@axcode/axcloud-sdk 0.1.7-dev.87ea138 → 0.1.7-dev.cc5cc69

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