@ledewire/browser 0.2.2 → 0.4.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
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@ledewire/browser)](https://www.npmjs.com/package/@ledewire/browser)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE)
5
5
 
6
- Browser SDK for the [LedeWire](https://api.ledewire.com/api-docs/index.html) content marketplace — embed buyer authentication, wallet funding, and content purchases on any website with a single script tag.
6
+ Browser SDK for the [LedeWire](https://api.ledewire.com/api-docs/index.html) content marketplace — embed buyer authentication, wallet funding, content purchases, and seller content discovery on any website with a single script tag.
7
7
 
8
8
  ## CDN — No Install Required
9
9
 
@@ -40,13 +40,26 @@ const lw = init({
40
40
 
41
41
  ## Client Namespaces
42
42
 
43
- | Namespace | Description |
44
- | -------------- | ------------------------------------------------ |
45
- | `lw.auth` | Buyer signup, email/password login, Google OAuth |
46
- | `lw.wallet` | Wallet balance, fund wallet via payment session |
47
- | `lw.purchases` | List and create content purchases |
48
- | `lw.content` | Fetch content with buyer access info |
49
- | `lw.checkout` | Checkout state — what action is required next |
43
+ | Namespace | Description |
44
+ | ------------------- | --------------------------------------------------------------- |
45
+ | `lw.config` | Platform public config (no auth required) |
46
+ | `lw.auth` | Buyer signup, email/password login, Google OAuth, API key login |
47
+ | `lw.wallet` | Wallet balance, fund wallet via payment session |
48
+ | `lw.purchases` | List and create content purchases |
49
+ | `lw.content` | Fetch content with buyer access info |
50
+ | `lw.checkout` | Checkout state — what action is required next |
51
+ | `lw.seller.content` | List, search, and get store content (API key auth) |
52
+
53
+ ## Example: Fetch Google OAuth Client ID Before Sign-In
54
+
55
+ ```ts
56
+ const lw = Ledewire.init({ apiKey: 'your_api_key' })
57
+
58
+ // No login needed — safe to call on page load
59
+ const { google_client_id } = await lw.config.getPublic()
60
+ google.accounts.id.initialize({ client_id: google_client_id, callback: handleCredential })
61
+ google.accounts.id.renderButton(document.getElementById('signin-btn'), { theme: 'outline' })
62
+ ```
50
63
 
51
64
  ## Example: Full Checkout Flow
52
65
 
@@ -80,6 +93,28 @@ switch (checkout_state.next_required_action) {
80
93
  }
81
94
  ```
82
95
 
96
+ ## Example: Seller Content Discovery
97
+
98
+ Use `lw.auth.loginWithApiKey` with only the `key` to obtain a read-only token,
99
+ then browse your store's content catalogue directly from the browser:
100
+
101
+ ```ts
102
+ const lw = Ledewire.init({ apiKey: 'your_api_key' })
103
+
104
+ // Obtain a view-only seller token (key only — no secret needed)
105
+ await lw.auth.loginWithApiKey({ key: 'your_api_key' })
106
+
107
+ // List all content
108
+ const items = await lw.seller.content.list()
109
+
110
+ // Search by title, URI, or metadata
111
+ const results = await lw.seller.content.search({ title: 'intro' })
112
+ const byMeta = await lw.seller.content.search({ metadata: { author: 'Alice' } })
113
+
114
+ // Fetch a single item
115
+ const item = await lw.seller.content.get('content-id')
116
+ ```
117
+
83
118
  ## Token Storage
84
119
 
85
120
  By default tokens are stored **in memory** (most secure — cleared on page unload).
package/dist/index.d.ts CHANGED
@@ -47,6 +47,9 @@ export declare class AuthError extends LedewireError {
47
47
  constructor(message: string, code?: number);
48
48
  }
49
49
 
50
+ /** Request body for API key authentication (seller). */
51
+ declare type AuthLoginApiKeyRequest = components['schemas']['AuthLoginApiKeyRequest'];
52
+
50
53
  /** Request body for buyer email/password login. */
51
54
  declare type AuthLoginEmailRequest = components['schemas']['AuthLoginEmailRequest'];
52
55
 
@@ -95,6 +98,27 @@ declare class BrowserAuthNamespace {
95
98
  * @returns The authentication token response.
96
99
  */
97
100
  loginWithGoogle(body: AuthLoginOAuthRequest): Promise<AuthenticationResponse>;
101
+ /**
102
+ * Log in using an API key to obtain a seller token.
103
+ * Provide only `key` for read-only (`view`) access.
104
+ * Provide both `key` and `secret` for read/write (`full`) access.
105
+ * Tokens are stored automatically after successful authentication.
106
+ *
107
+ * Use this before calling `lw.seller.content.*` methods.
108
+ *
109
+ * @param body - API key credentials.
110
+ * @returns The authentication token response.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * // View access (read-only) — sufficient for seller.content.list/search/get
115
+ * await lw.auth.loginWithApiKey({ key: 'your_api_key' })
116
+ *
117
+ * // Full access (read/write)
118
+ * await lw.auth.loginWithApiKey({ key: 'your_api_key', secret: 'your_secret' })
119
+ * ```
120
+ */
121
+ loginWithApiKey(body: AuthLoginApiKeyRequest): Promise<AuthenticationResponse>;
98
122
  private storeTokens;
99
123
  }
100
124
 
@@ -109,6 +133,8 @@ declare class BrowserClient {
109
133
  readonly _http: HttpClient;
110
134
  readonly _tokenManager: TokenManager;
111
135
  readonly _config: BrowserClientConfig;
136
+ /** Platform-level public configuration (no auth required) */
137
+ readonly config: BrowserConfigNamespace;
112
138
  /** Buyer authentication: email/password signup/login, Google, password reset */
113
139
  readonly auth: BrowserAuthNamespace;
114
140
  /** Checkout state machine: determines next action for a piece of content */
@@ -119,6 +145,8 @@ declare class BrowserClient {
119
145
  readonly purchases: BrowserPurchasesNamespace;
120
146
  /** Public content with per-user access information */
121
147
  readonly content: BrowserContentNamespace;
148
+ /** Seller content: list, search, and get (requires API key view token) */
149
+ readonly seller: BrowserSellerNamespace;
122
150
  /* Excluded from this release type: __constructor */
123
151
  }
124
152
 
@@ -160,6 +188,31 @@ export declare interface BrowserClientConfig {
160
188
  onAuthExpired?: () => void;
161
189
  }
162
190
 
191
+ /**
192
+ * Platform-level configuration for browser clients.
193
+ *
194
+ * Obtain via `client.config` — do not construct directly.
195
+ */
196
+ declare class BrowserConfigNamespace {
197
+ protected readonly http: HttpClient;
198
+ /* Excluded from this release type: __constructor */
199
+ /**
200
+ * Returns platform-level public configuration.
201
+ * No authentication required — safe to call before the user has signed in.
202
+ *
203
+ * Use `google_client_id` to initialise the Google Identity Services library
204
+ * on the storefront login page without requiring a prior authenticated call.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const lw = init({ apiKey: 'your_api_key' })
209
+ * const { google_client_id } = await lw.config.getPublic()
210
+ * google.accounts.id.initialize({ client_id: google_client_id, callback })
211
+ * ```
212
+ */
213
+ getPublic(): Promise<PublicConfigResponse>;
214
+ }
215
+
163
216
  /**
164
217
  * Buyer content namespace — fetch content with real-time access information.
165
218
  *
@@ -221,6 +274,85 @@ declare class BrowserPurchasesNamespace {
221
274
  get(id: string): Promise<PurchaseResponse>;
222
275
  }
223
276
 
277
+ /**
278
+ * Seller content namespace — list, search, and fetch store content by API key.
279
+ *
280
+ * Obtain via `lw.seller.content` — do not construct directly.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * const lw = Ledewire.init({ apiKey: 'your_api_key' })
285
+ * await lw.auth.loginWithApiKey({ key: 'your_api_key' })
286
+ *
287
+ * const items = await lw.seller.content.list()
288
+ * const results = await lw.seller.content.search({ title: 'intro' })
289
+ * const item = await lw.seller.content.get('content-id')
290
+ * ```
291
+ */
292
+ declare class BrowserSellerContentNamespace {
293
+ private readonly http;
294
+ /* Excluded from this release type: __constructor */
295
+ /**
296
+ * List all content for the authenticated seller's store.
297
+ * Requires a token with at least `view` permission.
298
+ *
299
+ * @returns Array of content items.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const items = await lw.seller.content.list()
304
+ * ```
305
+ */
306
+ list(): Promise<ContentResponse[]>;
307
+ /**
308
+ * Search content by title, URI, and/or metadata (AND logic).
309
+ * At least one criterion must be supplied.
310
+ * Requires a token with at least `view` permission.
311
+ *
312
+ * @param body - Search criteria (title, uri, and/or metadata).
313
+ * @returns Array of matching content items.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * const results = await lw.seller.content.search({ title: 'intro' })
318
+ * const byUri = await lw.seller.content.search({ uri: 'vimeo.com' })
319
+ * const byMeta = await lw.seller.content.search({ metadata: { author: 'Alice' } })
320
+ * ```
321
+ */
322
+ search(body: SellerContentSearchRequest): Promise<ContentResponse[]>;
323
+ /**
324
+ * Get a single content item by ID.
325
+ * Requires a token with at least `view` permission.
326
+ *
327
+ * @param id - The content ID.
328
+ * @returns The content item.
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * const item = await lw.seller.content.get('content-id')
333
+ * ```
334
+ */
335
+ get(id: string): Promise<ContentResponse>;
336
+ }
337
+
338
+ /**
339
+ * Seller operations accessible with a view-permission API key token.
340
+ *
341
+ * Obtain via `lw.seller` — do not construct directly.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const lw = Ledewire.init({ apiKey: 'your_api_key' })
346
+ * await lw.auth.loginWithApiKey({ key: 'your_api_key' })
347
+ * const items = await lw.seller.content.list()
348
+ * ```
349
+ */
350
+ declare class BrowserSellerNamespace {
351
+ /** Seller content: list, search, and get by API key. */
352
+ readonly content: BrowserSellerContentNamespace;
353
+ /* Excluded from this release type: __constructor */
354
+ }
355
+
224
356
  /**
225
357
  * Buyer wallet namespace — balance, transactions, and payment session management.
226
358
  *
@@ -315,6 +447,11 @@ export declare type CheckoutStateResponse = components['schemas']['CheckoutState
315
447
 
316
448
  declare interface components {
317
449
  schemas: {
450
+ /** @description Platform-level public configuration. No authentication required. */
451
+ PublicConfigResponse: {
452
+ /** @description Google OAuth client ID for initialising the Google Identity Services library. */
453
+ google_client_id: string;
454
+ };
318
455
  ContentAccessInfo: {
319
456
  user_id: string | null;
320
457
  has_purchased: boolean;
@@ -335,7 +472,7 @@ declare interface components {
335
472
  */
336
473
  expires_at: string;
337
474
  };
338
- /** @description Token response for merchant authentication. Tokens are store-agnostic — no store_context is embedded. */
475
+ /** @description Token response for merchant authentication. Includes stores the user has access to so the client can prompt for store selection. */
339
476
  MerchantAuthenticationResponse: {
340
477
  /** @enum {string} */
341
478
  token_type: 'Bearer';
@@ -343,6 +480,18 @@ declare interface components {
343
480
  refresh_token: string;
344
481
  /** Format: date-time */
345
482
  expires_at: string;
483
+ /** @description Stores the authenticated user can manage, for store-selection on the client. */
484
+ stores: components['schemas']['MerchantLoginStore'][];
485
+ };
486
+ /** @description A store the authenticated merchant user has access to. */
487
+ MerchantLoginStore: {
488
+ id: string;
489
+ name: string;
490
+ /**
491
+ * @description Derived role. `owner` if the user owns this store; `author` if they have author permissions only.
492
+ * @enum {string}
493
+ */
494
+ role: 'owner' | 'author';
346
495
  };
347
496
  JWTClaims: {
348
497
  /** @description User or Store identifier */
@@ -411,11 +560,14 @@ declare interface components {
411
560
  refresh_token?: string;
412
561
  };
413
562
  ManageableStore: {
414
- store_id: string;
415
- store_name: string;
563
+ id: string;
564
+ name: string;
416
565
  store_key: string;
417
- /** @enum {string|null} */
418
- role: 'owner' | null;
566
+ /**
567
+ * @description Derived role. `owner` if the user owns this store; `author` if they have author permissions only.
568
+ * @enum {string}
569
+ */
570
+ role: 'owner' | 'author';
419
571
  /** @description Whether the authenticated user has author permissions for this store. */
420
572
  is_author: boolean;
421
573
  logo: string | null;
@@ -425,12 +577,14 @@ declare interface components {
425
577
  user_id: string | null;
426
578
  store_id: string;
427
579
  /**
428
- * @description Named role. `owner` is the only valid named role; non-owner authors have a null role.
580
+ * @description Ownership flag expressed as a string. `owner` when `is_owner` is true; `null` for non-owner members (authors or uninvited users).
429
581
  * @enum {string|null}
430
582
  */
431
583
  role: 'owner' | null;
432
584
  /** @description Whether this user can create and manage their own content. */
433
585
  is_author: boolean;
586
+ /** @description Per-author fee override in basis points (1/100th of a percent). Overrides the store's `default_author_fee_bps` when set. `null` means the store default applies. */
587
+ author_fee_bps: number | null;
434
588
  /** Format: date-time */
435
589
  invited_at: string | null;
436
590
  /** Format: date-time */
@@ -445,6 +599,8 @@ declare interface components {
445
599
  * @default true
446
600
  */
447
601
  is_author: boolean;
602
+ /** @description Per-author fee override in basis points (0–10000). Overrides the store's `default_author_fee_bps`. `null` or omitted means the store default applies. */
603
+ author_fee_bps?: number | null;
448
604
  };
449
605
  MerchantInviteResponse: {
450
606
  invitation_token: string;
@@ -452,6 +608,8 @@ declare interface components {
452
608
  /** @enum {string|null} */
453
609
  role: 'owner' | null;
454
610
  is_author: boolean;
611
+ /** @description Per-author fee override set at invitation time. */
612
+ author_fee_bps?: number | null;
455
613
  /** Format: date-time */
456
614
  expires_at: string;
457
615
  };
@@ -541,7 +699,7 @@ declare interface components {
541
699
  */
542
700
  content_body?: string;
543
701
  /**
544
- * @description URI of the external resource. For `external_ref` content only.
702
+ * @description URI of the resource. Required for `external_ref` content; optional for `markdown` content.
545
703
  * @example https://vimeo.com/123456789
546
704
  */
547
705
  content_uri?: string;
@@ -773,7 +931,7 @@ declare interface components {
773
931
  */
774
932
  started_at?: string;
775
933
  };
776
- /** @description Create request body for content. Required fields vary by `content_type`: `markdown` requires `content_body`; `external_ref` requires `content_uri`. */
934
+ /** @description Create request body for content. Required fields vary by `content_type`: `markdown` requires `content_body`; `external_ref` requires `content_uri`. Both types accept an optional `content_uri` link. */
777
935
  Content: {
778
936
  /**
779
937
  * @description The type of content being created.
@@ -788,7 +946,7 @@ declare interface components {
788
946
  */
789
947
  content_body?: string;
790
948
  /**
791
- * @description URI of the external resource (Vimeo, YouTube, PDF, etc.). Required when `content_type` is `external_ref`.
949
+ * @description URI of the resource. Required when `content_type` is `external_ref`; optional for `markdown` content.
792
950
  * @example https://vimeo.com/123456789
793
951
  */
794
952
  content_uri?: string;
@@ -880,7 +1038,60 @@ declare interface components {
880
1038
  [key: string]: unknown;
881
1039
  };
882
1040
  };
883
- /** @description Lightweight content representation returned by list and search endpoints. Omits body fields to keep list payloads small. Use the detail endpoint (`GET /v1/merchant/:store_id/content/:id`) to retrieve the full body or URI. */
1041
+ /** @description Pagination metadata returned on all paginated list endpoints. */
1042
+ PaginationMeta: {
1043
+ /**
1044
+ * @description Total number of records matching the query.
1045
+ * @example 84
1046
+ */
1047
+ total: number;
1048
+ /**
1049
+ * @description Maximum number of records returned per page.
1050
+ * @example 25
1051
+ */
1052
+ per_page: number;
1053
+ /**
1054
+ * @description The current page number (1-based).
1055
+ * @example 2
1056
+ */
1057
+ current_page: number;
1058
+ /**
1059
+ * @description Total number of pages.
1060
+ * @example 4
1061
+ */
1062
+ total_pages: number;
1063
+ /**
1064
+ * @description Next page number, or null if on the last page.
1065
+ * @example 3
1066
+ */
1067
+ next_page?: number | null;
1068
+ /**
1069
+ * @description Previous page number, or null if on the first page.
1070
+ * @example 1
1071
+ */
1072
+ prev_page?: number | null;
1073
+ };
1074
+ /** @description Paginated list of content items. */
1075
+ PaginatedContentList: {
1076
+ data: components['schemas']['ContentListItem'][];
1077
+ pagination: components['schemas']['PaginationMeta'];
1078
+ };
1079
+ /** @description Paginated list of content sales statistics. */
1080
+ PaginatedSalesList: {
1081
+ data: components['schemas']['SalesStatisticsItem'][];
1082
+ pagination: components['schemas']['PaginationMeta'];
1083
+ };
1084
+ /** @description Paginated list of anonymised buyer statistics. */
1085
+ PaginatedBuyersList: {
1086
+ data: components['schemas']['BuyerStatisticsItem'][];
1087
+ pagination: components['schemas']['PaginationMeta'];
1088
+ };
1089
+ /** @description Paginated list of store members. */
1090
+ PaginatedUsersList: {
1091
+ data: components['schemas']['MerchantUser'][];
1092
+ pagination: components['schemas']['PaginationMeta'];
1093
+ };
1094
+ /** @description Lightweight content representation returned by list and search endpoints. Omits `content_body` to keep list payloads small. Use the detail endpoint (`GET /v1/merchant/:store_id/content/:id`) to retrieve the full body. */
884
1095
  ContentListItem: {
885
1096
  id: string;
886
1097
  /** @enum {string} */
@@ -896,6 +1107,8 @@ declare interface components {
896
1107
  visibility: 'public' | 'unlisted' | 'private';
897
1108
  /** Format: date-time */
898
1109
  created_at: string;
1110
+ /** @description External URI for `external_ref` content; optional supplementary link for `markdown` content. Null when not set. */
1111
+ content_uri: string | null;
899
1112
  /** @description Namespaced platform ID for `external_ref` content. Null for other types. */
900
1113
  external_identifier?: string | null;
901
1114
  };
@@ -1033,12 +1246,42 @@ declare interface components {
1033
1246
  /** Checkout state for a specific piece of content and user. */
1034
1247
  export declare type ContentAccessInfo = components['schemas']['ContentAccessInfo'];
1035
1248
 
1249
+ /** Full content item returned by all seller content endpoints. */
1250
+ declare type ContentResponse = components['schemas']['ContentResponse'];
1251
+
1036
1252
  /** A piece of content with buyer access information. */
1037
1253
  export declare type ContentWithAccessResponse = components['schemas']['ContentWithAccessResponse'];
1038
1254
 
1039
1255
  /**
1040
1256
  * Thrown when the authenticated user does not have permission
1041
1257
  * to perform the requested operation.
1258
+ *
1259
+ * **Common cause — merchant login with the wrong account role:**
1260
+ * `POST /v1/auth/merchant/login/email` and `POST /v1/auth/merchant/login/google`
1261
+ * return `403 Forbidden` (not `401`) when the credentials are valid but the
1262
+ * account has no merchant store associations (e.g. a buyer account used on the
1263
+ * merchant endpoint). The `err.message` will be:
1264
+ * `"This account does not have merchant access. Use a merchant or owner account."`
1265
+ *
1266
+ * Use a different account or ask a store owner to add your account as a member.
1267
+ *
1268
+ * @example
1269
+ * ```ts
1270
+ * import { ForbiddenError, AuthError } from '@ledewire/node'
1271
+ *
1272
+ * try {
1273
+ * await client.merchant.auth.loginWithGoogle({ id_token })
1274
+ * } catch (err) {
1275
+ * if (err instanceof ForbiddenError) {
1276
+ * // Credentials were valid but account has no merchant store access.
1277
+ * // err.message → "This account does not have merchant access. Use a merchant or owner account."
1278
+ * console.error('Wrong account role:', err.message)
1279
+ * } else if (err instanceof AuthError) {
1280
+ * // Bad credentials or expired token — re-authenticate.
1281
+ * console.error('Authentication failed:', err.message)
1282
+ * }
1283
+ * }
1284
+ * ```
1042
1285
  */
1043
1286
  export declare class ForbiddenError extends LedewireError {
1044
1287
  constructor(message: string, code?: number);
@@ -1215,6 +1458,13 @@ export declare class NotFoundError extends LedewireError {
1215
1458
  */
1216
1459
  export declare function parseExpiresAt(expiresAt: string): number;
1217
1460
 
1461
+ /**
1462
+ * Platform-level public configuration returned by `GET /v1/config/public`.
1463
+ * No authentication required. Use this to get the Google OAuth client ID
1464
+ * before the user has signed in.
1465
+ */
1466
+ export declare type PublicConfigResponse = components['schemas']['PublicConfigResponse'];
1467
+
1218
1468
  /** Request body for creating a purchase. */
1219
1469
  export declare type PurchaseCreateRequest = components['schemas']['PurchaseCreateRequest'];
1220
1470
 
@@ -1229,6 +1479,19 @@ export declare class PurchaseError extends LedewireError {
1229
1479
  /** A purchase record. */
1230
1480
  export declare type PurchaseResponse = components['schemas']['PurchaseResponse'];
1231
1481
 
1482
+ /**
1483
+ * Search criteria for seller content search.
1484
+ * At least one field must be supplied. All supplied fields must match (AND logic).
1485
+ */
1486
+ export declare interface SellerContentSearchRequest {
1487
+ /** Case-insensitive partial match against the content title. */
1488
+ title?: string;
1489
+ /** Case-insensitive partial match against the content URI (`external_ref` content only). */
1490
+ uri?: string;
1491
+ /** Exact key/value pairs to AND-match against content metadata. */
1492
+ metadata?: Record<string, unknown>;
1493
+ }
1494
+
1232
1495
  /** Internal representation of stored authentication tokens. */
1233
1496
  export declare interface StoredTokens {
1234
1497
  accessToken: string;
@@ -1283,13 +1546,18 @@ declare interface TokenManagerOptions {
1283
1546
  */
1284
1547
  refreshFn: (refreshToken: string) => Promise<StoredTokens>;
1285
1548
  /**
1286
- * Called after a successful token refresh.
1287
- * Use in server environments to persist the new tokens to a database or cache.
1549
+ * Called after a successful background token refresh (not on initial login).
1550
+ * Use for side-effects only — e.g. audit logging, cache invalidation, or
1551
+ * notifying a secondary system when tokens rotate.
1552
+ *
1553
+ * **Do not use this for token persistence.** The `storage` adapter's
1554
+ * `setTokens` is the canonical persistence hook and is already called on
1555
+ * every refresh. Providing both will result in double-writes.
1288
1556
  *
1289
1557
  * @example
1290
1558
  * ```ts
1291
1559
  * onTokenRefreshed: async (tokens) => {
1292
- * await redis.set('session:tokens', JSON.stringify(tokens))
1560
+ * await auditLog.record('token_refreshed', { expiresAt: tokens.expiresAt })
1293
1561
  * }
1294
1562
  * ```
1295
1563
  */