@baasix/sdk 0.1.19 → 0.1.21

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
@@ -9,7 +9,7 @@ Official JavaScript/TypeScript SDK for [Baasix](https://www.baasix.com) Backend-
9
9
  ## Features
10
10
 
11
11
  - 🌐 **Universal** - Works in browsers, Node.js, and React Native
12
- - 🔐 **Flexible Auth** - JWT tokens, HTTP-only cookies, OAuth (Google, Facebook, Apple, GitHub)
12
+ - 🔐 **Flexible Auth** - JWT tokens, HTTP-only cookies, 35+ OAuth providers (Google, GitHub, Discord, Microsoft, Slack, and more), passkeys (WebAuthn), and 2FA (TOTP + backup codes)
13
13
  - 💾 **Customizable Storage** - LocalStorage, AsyncStorage, or custom adapters
14
14
  - 📝 **Type-Safe** - Full TypeScript support with generics
15
15
  - 🔄 **Auto Token Refresh** - Seamless token management
@@ -23,6 +23,7 @@ Official JavaScript/TypeScript SDK for [Baasix](https://www.baasix.com) Backend-
23
23
  - 🔔 **Notifications** - User notification system with realtime delivery
24
24
  - 🗃️ **Migrations** - Database schema migration management
25
25
  - 🔃 **Sort/Reorder** - Drag-and-drop style item reordering
26
+ - 🧩 **App Builder** - Create and manage pages & blocks via the items API
26
27
 
27
28
  ## Installation
28
29
 
@@ -416,6 +417,65 @@ const results = await baasix.items('orders').aggregate({
416
417
  });
417
418
  ```
418
419
 
420
+ ## App Builder (Pages & Blocks)
421
+
422
+ The Baasix [App Builder](https://baasix.dev/docs/guides/app-builder) lets admins compose internal
423
+ tools from 27 data-bound block types (tables, wizard forms, kanban, charts, tabs, modals, reports,
424
+ and more) with cross-block reactivity on a 12-column grid. Pages and blocks are stored in the
425
+ `baasix_Page` and `baasix_Block` system collections, so you manage them with the **same `items()`
426
+ API** — no special client. Block `config` is validated server-side on every create and update.
427
+
428
+ ```typescript
429
+ // Create a page
430
+ const page = await baasix.items('baasix_Page').create({
431
+ name: 'Operations',
432
+ slug: 'operations', // unique per tenant; used in /pages/?slug=operations
433
+ icon: 'gauge',
434
+ });
435
+
436
+ // Add a table block bound to the orders collection
437
+ await baasix.items('baasix_Block').create({
438
+ page_Id: page.id,
439
+ type: 'table',
440
+ collection: 'orders',
441
+ position: { row: 0, col: 0, span: 8 }, // 12-column grid: row, col, span
442
+ config: {
443
+ columns: [{ field: 'id' }, { field: 'customer' }, { field: 'total' }],
444
+ filter: { status: { eq: 'open' } },
445
+ sort: 'createdAt:desc',
446
+ actions: { create: true, edit: true, view: true },
447
+ },
448
+ });
449
+
450
+ // Add a status chart beside it
451
+ await baasix.items('baasix_Block').create({
452
+ page_Id: page.id,
453
+ type: 'chart',
454
+ collection: 'orders',
455
+ position: { row: 0, col: 8, span: 4 },
456
+ config: {
457
+ chartType: 'doughnut',
458
+ aggregate: { count: { function: 'count', field: 'id' } },
459
+ groupBy: ['status'],
460
+ },
461
+ });
462
+
463
+ // List all blocks on a page
464
+ const blocks = await baasix.items('baasix_Block')
465
+ .filter({ page_Id: { eq: page.id } })
466
+ .get();
467
+ ```
468
+
469
+ > **Block types (27):** data — `table`, `form`, `details`, `kanban`, `calendar`, `chart`, `cardlist`,
470
+ > `map` (Leaflet or Google Maps), `geochart`, `media`, `feed`, `timeline`, `progress`, `repeater`,
471
+ > `report`, `filter`; layout — `tabs`, `container`, `modal`, `divider` (children carry
472
+ > `parentBlock_Id` + `slot`); content & input — `markdown`, `richtext`, `buttons`, `input`,
473
+ > `iframe`, `upload`, `code`.
474
+ > Filters in any block config use the same filter DSL as the items API, plus the runtime
475
+ > placeholders `"$param.<name>"`, `"$selection.<blockId>.<field>"` and `"$input.<name>"` for
476
+ > master-detail wiring. See the
477
+ > [App Builder guide](https://baasix.dev/docs/guides/app-builder) for the full config reference.
478
+
419
479
  ## Filter Operators
420
480
 
421
481
  Baasix supports 50+ filter operators:
@@ -1181,24 +1241,107 @@ baasix.realtime.disconnect();
1181
1241
 
1182
1242
  ## OAuth / Social Login
1183
1243
 
1244
+ 35 social providers are supported via the `SOCIAL_PROVIDERS` constant — `google`, `github`, `discord`, `microsoft`, `slack`, `apple`, `facebook`, `gitlab`, `linkedin`, `twitter`, … 35 total. `getOAuthUrl`'s `provider` option is typed as `OAuthProvider`, the union of all 35 ids — import `SOCIAL_PROVIDERS` if you need the list at runtime (e.g. to render login buttons).
1245
+
1246
+ End-to-end browser flow: `getOAuthUrl()` builds the provider redirect URL → the browser navigates there and completes the provider's login → the provider redirects back to your app's callback page with a `token` (or `error`) query param → your callback page calls `handleOAuthCallback(token)` to finish signing in.
1247
+
1184
1248
  ```typescript
1185
- // Redirect to OAuth provider
1249
+ import { SOCIAL_PROVIDERS, type OAuthProvider } from '@baasix/sdk';
1250
+
1251
+ // SOCIAL_PROVIDERS: readonly array of all 35 provider ids, e.g. for rendering buttons
1252
+ SOCIAL_PROVIDERS.forEach((provider) => renderSocialButton(provider));
1253
+
1254
+ // 1. Redirect to the OAuth provider
1186
1255
  const url = baasix.auth.getOAuthUrl({
1187
- provider: 'google', // 'google' | 'facebook' | 'apple' | 'github'
1256
+ provider: 'github', // OAuthProvider — one of SOCIAL_PROVIDERS (35 total)
1188
1257
  redirectUrl: 'https://myapp.com/auth/callback',
1189
1258
  });
1190
1259
  window.location.href = url;
1191
1260
 
1192
- // In your callback page
1261
+ // 2. Provider redirects back to your callback page with ?token=... (or ?error=...)
1262
+ // In your callback page:
1193
1263
  const params = new URLSearchParams(window.location.search);
1194
1264
  const token = params.get('token');
1265
+ const error = params.get('error');
1195
1266
 
1196
1267
  if (token) {
1197
1268
  const { user } = await baasix.auth.handleOAuthCallback(token);
1198
1269
  console.log('Logged in as:', user.email);
1270
+ } else if (error) {
1271
+ console.error('OAuth sign-in failed:', error);
1272
+ }
1273
+ ```
1274
+
1275
+ ## Auth Method Discovery
1276
+
1277
+ Ask the server which login methods are actually enabled (registration, email/password, magic link, passkey, 2FA, and the list of configured social providers) so your login UI only shows what's available. The result is cached per client instance — pass `force: true` to bypass the cache.
1278
+
1279
+ ```typescript
1280
+ const methods = await baasix.auth.getAuthMethods();
1281
+ // {
1282
+ // registration: true,
1283
+ // emailPassword: true,
1284
+ // magicLink: false,
1285
+ // passkey: true,
1286
+ // twoFactor: true,
1287
+ // socialProviders: ['google', 'github', 'discord'],
1288
+ // }
1289
+
1290
+ if (methods.socialProviders.includes('google')) {
1291
+ // show "Sign in with Google"
1292
+ }
1293
+ if (methods.passkey) {
1294
+ // show "Sign in with a passkey"
1199
1295
  }
1200
1296
  ```
1201
1297
 
1298
+ ## Two-Factor Authentication
1299
+
1300
+ TOTP-based 2FA (SHA1, 6-digit codes, 30s window) with 10 single-use backup codes. 2FA gates **password login only** — social, magic-link, and passkey sign-ins bypass the 2FA challenge (they're already a possession/federated factor). Enabling 2FA requires the account to already have a password credential.
1301
+
1302
+ ```typescript
1303
+ // Enable 2FA (while logged in) — returns a secret, otpauth:// URL (render as a QR code), and backup codes
1304
+ const setup = await baasix.auth.twoFactor.enable();
1305
+ // { secret, otpauthUrl, backupCodes: [10 codes] }
1306
+
1307
+ // Confirm setup with a code from the authenticator app
1308
+ await baasix.auth.twoFactor.verifySetup('123456');
1309
+ // { enabled: true }
1310
+
1311
+ // Disable 2FA (requires the account password)
1312
+ await baasix.auth.twoFactor.disable('currentPassword');
1313
+ // { disabled: true }
1314
+
1315
+ // Login flow with 2FA enabled
1316
+ const result = await baasix.auth.login({ email, password });
1317
+
1318
+ if ('twoFactorRequired' in result && result.twoFactorRequired) {
1319
+ // Prompt for a TOTP code or an unused backup code
1320
+ const { user, token } = await baasix.auth.twoFactor.verify(result.twoFactorToken, '123456');
1321
+ } else {
1322
+ // Logged in directly — 2FA not enabled for this account
1323
+ const { user, token } = result;
1324
+ }
1325
+ ```
1326
+
1327
+ ## Passkeys
1328
+
1329
+ WebAuthn-based, usernameless passkey sign-in with per-user credential management. **Browser-only** — `baasix.auth.passkey.*` methods dynamically import `@simplewebauthn/browser` and throw a `BaasixError` when called outside a browser (e.g. Node.js or React Native).
1330
+
1331
+ ```typescript
1332
+ // Register a passkey for the currently logged-in user
1333
+ await baasix.auth.passkey.register('MacBook Touch ID'); // name is optional
1334
+
1335
+ // Authenticate with a passkey (no prior login required)
1336
+ const { user, token } = await baasix.auth.passkey.authenticate();
1337
+
1338
+ // List the current user's registered passkeys (no key material returned)
1339
+ const passkeys = await baasix.auth.passkey.list();
1340
+
1341
+ // Remove one of your own passkeys
1342
+ await baasix.auth.passkey.remove(passkeys[0].id);
1343
+ ```
1344
+
1202
1345
  ## Invitation System (Multi-tenant)
1203
1346
 
1204
1347
  ```typescript
package/dist/index.cjs CHANGED
@@ -192,7 +192,7 @@ var HttpClient = class {
192
192
  } catch {
193
193
  }
194
194
  const message = errorData.error?.message || errorData.message || response.statusText || "Request failed";
195
- const code = errorData.error?.code;
195
+ const code = errorData.error?.code ?? errorData.code;
196
196
  const details = errorData.details;
197
197
  return new BaasixError(message, response.status, code, details);
198
198
  }
@@ -476,17 +476,123 @@ var MemoryStorageAdapter = class {
476
476
  };
477
477
 
478
478
  // src/modules/auth.ts
479
+ var SOCIAL_PROVIDERS = ["apple", "atlassian", "cognito", "discord", "dropbox", "facebook", "figma", "github", "gitlab", "google", "huggingface", "kakao", "kick", "line", "linear", "linkedin", "microsoft", "naver", "notion", "paybin", "paypal", "polar", "railway", "reddit", "roblox", "salesforce", "slack", "spotify", "tiktok", "twitch", "twitter", "vercel", "vk", "wechat", "zoom"];
479
480
  var AuthModule = class {
480
481
  client;
481
482
  storage;
482
483
  authMode;
483
484
  onAuthStateChange;
484
485
  currentUser = null;
486
+ authMethodsCache = null;
487
+ /**
488
+ * Two-factor authentication (TOTP) helpers.
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * const result = await baasix.auth.login({ email, password });
493
+ * if ("twoFactorRequired" in result && result.twoFactorRequired) {
494
+ * const { user, token } = await baasix.auth.twoFactor.verify(result.twoFactorToken, code);
495
+ * }
496
+ * ```
497
+ */
498
+ twoFactor;
499
+ /**
500
+ * Passkey (WebAuthn) registration and authentication helpers.
501
+ * Browser-only: throws a `BaasixError` when called outside a browser
502
+ * environment (no `window`/`PublicKeyCredential`).
503
+ *
504
+ * @example
505
+ * ```typescript
506
+ * // Register a passkey for the currently logged-in user
507
+ * await baasix.auth.passkey.register("My Laptop");
508
+ *
509
+ * // Authenticate with a passkey (no prior login required)
510
+ * const { user, token } = await baasix.auth.passkey.authenticate();
511
+ *
512
+ * // List and remove registered passkeys
513
+ * const passkeys = await baasix.auth.passkey.list();
514
+ * await baasix.auth.passkey.remove(passkeys[0].id);
515
+ * ```
516
+ */
517
+ passkey = {
518
+ register: async (name) => {
519
+ this.assertBrowser();
520
+ const { startRegistration } = await import('@simplewebauthn/browser');
521
+ const options = await this.client.post("/auth/passkey/register/options", {});
522
+ const attestation = await startRegistration({ optionsJSON: options });
523
+ await this.client.post("/auth/passkey/register/verify", {
524
+ response: attestation,
525
+ name: name || null
526
+ });
527
+ return { verified: true };
528
+ },
529
+ authenticate: async (opts) => {
530
+ this.assertBrowser();
531
+ const { startAuthentication } = await import('@simplewebauthn/browser');
532
+ const { options, challengeId } = await this.client.post(
533
+ "/auth/passkey/authenticate/options",
534
+ {},
535
+ { skipAuth: true }
536
+ );
537
+ const assertion = await startAuthentication({ optionsJSON: options });
538
+ const response = await this.client.post(
539
+ "/auth/passkey/authenticate/verify",
540
+ { challengeId, response: assertion, authMode: opts?.authMode },
541
+ { skipAuth: true }
542
+ );
543
+ await this.storeTokens(response);
544
+ this.emitAuthStateChange("SIGNED_IN", response.user);
545
+ return response;
546
+ },
547
+ list: async () => (await this.client.get("/auth/passkey")).passkeys,
548
+ remove: async (id) => {
549
+ await this.client.delete(`/auth/passkey/${encodeURIComponent(id)}`);
550
+ }
551
+ };
485
552
  constructor(config) {
486
553
  this.client = config.client;
487
554
  this.storage = config.storage;
488
555
  this.authMode = config.authMode;
489
556
  this.onAuthStateChange = config.onAuthStateChange;
557
+ this.twoFactor = {
558
+ verify: async (twoFactorToken, code, opts) => {
559
+ const response = await this.client.post(
560
+ "/auth/2fa/verify",
561
+ {
562
+ twoFactorToken,
563
+ code,
564
+ authMode: opts?.authMode,
565
+ authType: opts?.authType
566
+ },
567
+ { skipAuth: true }
568
+ );
569
+ await this.storeTokens(response);
570
+ this.emitAuthStateChange("SIGNED_IN", response.user);
571
+ return response;
572
+ },
573
+ enable: () => {
574
+ return this.client.post("/auth/2fa/enable");
575
+ },
576
+ verifySetup: (code) => {
577
+ return this.client.post("/auth/2fa/verify-setup", {
578
+ code
579
+ });
580
+ },
581
+ disable: (password) => {
582
+ return this.client.post("/auth/2fa/disable", {
583
+ password
584
+ });
585
+ }
586
+ };
587
+ }
588
+ /**
589
+ * Assert that we're running in a browser environment with WebAuthn support.
590
+ * Throws a `BaasixError` otherwise.
591
+ */
592
+ assertBrowser() {
593
+ if (typeof window === "undefined" || !window.PublicKeyCredential) {
594
+ throw new BaasixError("Passkeys are only available in a browser", 400);
595
+ }
490
596
  }
491
597
  /**
492
598
  * Emit an authentication state change event
@@ -583,9 +689,39 @@ var AuthModule = class {
583
689
  },
584
690
  { skipAuth: true }
585
691
  );
586
- await this.storeTokens(response);
587
- this.emitAuthStateChange("SIGNED_IN", response.user);
588
- return response;
692
+ if ("twoFactorRequired" in response && response.twoFactorRequired) {
693
+ return response;
694
+ }
695
+ const authResponse = response;
696
+ await this.storeTokens(authResponse);
697
+ this.emitAuthStateChange("SIGNED_IN", authResponse.user);
698
+ return authResponse;
699
+ }
700
+ /**
701
+ * Discover which authentication methods are enabled for this project
702
+ * (registration, email/password, magic link, passkey, 2FA, and the list
703
+ * of configured social/OAuth providers). Result is cached per instance;
704
+ * pass `force: true` to bypass the cache and refetch.
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * const methods = await baasix.auth.getAuthMethods();
709
+ * if (methods.socialProviders.includes("google")) {
710
+ * // show "Sign in with Google"
711
+ * }
712
+ * ```
713
+ */
714
+ async getAuthMethods(force = false) {
715
+ if (this.authMethodsCache && !force) {
716
+ return this.authMethodsCache;
717
+ }
718
+ const response = await this.client.get(
719
+ "/",
720
+ { skipAuth: true }
721
+ );
722
+ const authMethods = response?.project?.auth;
723
+ this.authMethodsCache = authMethods;
724
+ return authMethods;
589
725
  }
590
726
  /**
591
727
  * Logout the current user
@@ -2574,9 +2710,21 @@ var PermissionsModule = class {
2574
2710
  * action: 'update',
2575
2711
  * fields: ['title', 'content', 'status'],
2576
2712
  * conditions: {
2577
- * author_Id: { eq: '$CURRENT_USER' }
2713
+ * author_Id: { eq: '$CURRENT_USER' } // USING: which rows may be updated
2714
+ * },
2715
+ * checkConditions: {
2716
+ * author_Id: { eq: '$CURRENT_USER' } // WITH CHECK: what they may become
2578
2717
  * }
2579
2718
  * });
2719
+ *
2720
+ * // Create grants use checkConditions (conditions is rejected on create):
2721
+ * await baasix.permissions.create({
2722
+ * role_Id: 'user-role-uuid',
2723
+ * collection: 'tasks',
2724
+ * action: 'create',
2725
+ * fields: ['*'],
2726
+ * checkConditions: { owner_Id: { eq: '$CURRENT_USER' } }
2727
+ * });
2580
2728
  * ```
2581
2729
  */
2582
2730
  async create(data) {
@@ -2634,9 +2782,9 @@ var PermissionsModule = class {
2634
2782
  * @example
2635
2783
  * ```typescript
2636
2784
  * await baasix.permissions.createCrudPermissions('role-uuid', 'products', {
2637
- * create: { fields: ['name', 'price', 'description'] },
2785
+ * create: { fields: ['name', 'price'], checkConditions: { owner_Id: { eq: '$CURRENT_USER' } } },
2638
2786
  * read: { fields: ['*'] },
2639
- * update: { fields: ['name', 'price', 'description'] },
2787
+ * update: { fields: ['name', 'price'], conditions: { owner_Id: { eq: '$CURRENT_USER' } } },
2640
2788
  * delete: false
2641
2789
  * });
2642
2790
  * ```
@@ -2647,12 +2795,15 @@ var PermissionsModule = class {
2647
2795
  for (const action of actions) {
2648
2796
  const actionConfig = config[action];
2649
2797
  if (actionConfig === false) continue;
2798
+ const cfg = actionConfig;
2650
2799
  const permission = await this.create({
2651
2800
  role_Id: roleId,
2652
2801
  collection,
2653
2802
  action,
2654
- fields: actionConfig?.fields || ["*"],
2655
- conditions: actionConfig?.conditions
2803
+ fields: cfg?.fields || ["*"],
2804
+ // `conditions` filters existing rows (USING) — never sent for create
2805
+ conditions: action === "create" ? void 0 : cfg?.conditions,
2806
+ checkConditions: action === "create" || action === "update" ? cfg?.checkConditions : void 0
2656
2807
  });
2657
2808
  permissions.push(permission);
2658
2809
  }
@@ -5105,6 +5256,7 @@ exports.RealtimeChannel = RealtimeChannel;
5105
5256
  exports.RealtimeModule = RealtimeModule;
5106
5257
  exports.ReportsModule = ReportsModule;
5107
5258
  exports.RolesModule = RolesModule;
5259
+ exports.SOCIAL_PROVIDERS = SOCIAL_PROVIDERS;
5108
5260
  exports.STORAGE_KEYS = STORAGE_KEYS;
5109
5261
  exports.SchemasModule = SchemasModule;
5110
5262
  exports.SettingsModule = SettingsModule;