@buildbase/sdk 0.0.89 → 0.0.90
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/AGENTS.md +7 -3
- package/CHANGELOG.md +505 -0
- package/README.md +120 -3
- package/dist/cli.mjs +1 -1
- package/dist/css/styles.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1350 -11
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.d.ts +120 -2
- package/dist/mcp/index.mjs +1 -1
- package/dist/react/chunks/SettingsDialog-BVyV5Zfm.mjs +2 -0
- package/dist/react/chunks/SettingsDialog-Ch8X6WXy.cjs +2 -0
- package/dist/react/chunks/{SubscriptionDialog-B8FTFRtp.cjs → SubscriptionDialog-CGIvMKpT.cjs} +1 -1
- package/dist/react/chunks/SubscriptionDialog-g0LwHhbe.mjs +2 -0
- package/dist/react/chunks/ar-3aqWucom.mjs +2 -0
- package/dist/react/chunks/ar-DQVB89r8.cjs +2 -0
- package/dist/react/chunks/de-DO10bhnU.cjs +2 -0
- package/dist/react/chunks/de-HXQk5D6k.mjs +2 -0
- package/dist/react/chunks/es-BZsmhBN3.mjs +2 -0
- package/dist/react/chunks/es-Cxg-qhUh.cjs +2 -0
- package/dist/react/chunks/fr-COCMy3vW.cjs +2 -0
- package/dist/react/chunks/fr-rdnNHzxy.mjs +2 -0
- package/dist/react/chunks/hi-BB0Z-Dgn.cjs +2 -0
- package/dist/react/chunks/hi-t3RH7ITu.mjs +2 -0
- package/dist/react/chunks/ja-CDyla2g0.mjs +2 -0
- package/dist/react/chunks/ja-D92JgUlk.cjs +2 -0
- package/dist/react/chunks/react-DTq7A_PT.mjs +2 -0
- package/dist/react/chunks/react-DeapSbq6.cjs +2 -0
- package/dist/react/chunks/zh-C5HABZZV.mjs +2 -0
- package/dist/react/chunks/zh-DK2yA5wB.cjs +2 -0
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.d.ts +1517 -14
- package/dist/react/index.mjs +1 -1
- package/docs/ERROR_CODES.md +218 -0
- package/docs/THEMING.md +89 -21
- package/docs/UI-CONFIG.md +23 -4
- package/docs/USER-IMPORT.md +320 -0
- package/package.json +2 -1
- package/dist/react/chunks/SettingsDialog-CKVE1PpH.mjs +0 -2
- package/dist/react/chunks/SettingsDialog-DrDmxFEj.cjs +0 -2
- package/dist/react/chunks/SubscriptionDialog-UN7o9Hqb.mjs +0 -2
- package/dist/react/chunks/ar-B2l90OZJ.cjs +0 -2
- package/dist/react/chunks/ar-DVVNZhXD.mjs +0 -2
- package/dist/react/chunks/de-BspJtvAC.mjs +0 -2
- package/dist/react/chunks/de-dURUqbl0.cjs +0 -2
- package/dist/react/chunks/es-DArjREi5.cjs +0 -2
- package/dist/react/chunks/es-vu4BmI3g.mjs +0 -2
- package/dist/react/chunks/fr-DVm5WCvZ.mjs +0 -2
- package/dist/react/chunks/fr-DhYDaYrX.cjs +0 -2
- package/dist/react/chunks/hi-C_nv_c1T.cjs +0 -2
- package/dist/react/chunks/hi-VW8xCj_K.mjs +0 -2
- package/dist/react/chunks/ja-C-b88dvw.mjs +0 -2
- package/dist/react/chunks/ja-CqtDkLHr.cjs +0 -2
- package/dist/react/chunks/react-CHivaJhA.mjs +0 -2
- package/dist/react/chunks/react-Ch_Suxxy.cjs +0 -2
- package/dist/react/chunks/zh-D1yfhUf0.mjs +0 -2
- package/dist/react/chunks/zh-D_tMpsNt.cjs +0 -2
package/README.md
CHANGED
|
@@ -13,6 +13,10 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
|
|
|
13
13
|
- [UI Configuration](#️-ui-configuration)
|
|
14
14
|
- [Authentication](#-authentication)
|
|
15
15
|
- [Redirect Preservation](#redirect-preservation)
|
|
16
|
+
- [Connected Accounts](#connected-accounts)
|
|
17
|
+
- [Phone Number and Text Message Codes](#phone-number-and-text-message-codes)
|
|
18
|
+
- [Confirm It's You, Deleting the Account and Owner Sessions](#confirm-its-you-deleting-the-account-and-owner-sessions)
|
|
19
|
+
- [Usernames and Guests](#usernames-and-guests)
|
|
16
20
|
- [Affiliate / Referral Tracking](#affiliate--referral-tracking)
|
|
17
21
|
- [Tracking & Tags](#-tracking--tags)
|
|
18
22
|
- [Role-Based Access Control](#-role-based-access-control)
|
|
@@ -22,6 +26,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
|
|
|
22
26
|
- [Push Notifications](#-push-notifications)
|
|
23
27
|
- [Notifications](#-notifications)
|
|
24
28
|
- [Content from your server](#️-content-from-your-server)
|
|
29
|
+
- [Importing Users](#-importing-users)
|
|
25
30
|
- [Connected Agents](#-connected-agents)
|
|
26
31
|
- [Devices & Sessions](#-devices--sessions)
|
|
27
32
|
- [User Management](#-user-management)
|
|
@@ -84,7 +89,7 @@ Everything the SDK lets you configure, override, or extend — one row per knob,
|
|
|
84
89
|
| Which SDK UI renders at all | `ui` prop: hide/show settings sections, workspace switcher, per-screen toggles | [UI Configuration](#️-ui-configuration) · `docs/UI-CONFIG.md` |
|
|
85
90
|
| Every user-facing string | `ui.messages` deep-override (e.g. rename "Subscription" → "Billing"), per-locale | [UI Configuration](#️-ui-configuration) |
|
|
86
91
|
| Language & number systems | `locale` prop — 8 locales, ICU plurals, RTL (`ar`), native numerals | [Internationalization](#-internationalization-i18n) |
|
|
87
|
-
| Look & feel | CSS custom properties (`--primary`, `--
|
|
92
|
+
| Look & feel | CSS custom properties (`--primary`, status fills such as `--success` and their text tones such as `--success-text`, …): semantic tokens, light/dark | `docs/THEMING.md` |
|
|
88
93
|
| Date/number formats | `ui.formats` (`Intl` options passed through) | `docs/UI-CONFIG.md` |
|
|
89
94
|
| Conditional UI | Gate components: `WhenAuthenticated`, `WhenPermission`, `WhenWorkspaceFeatureEnabled`, `WhenSubscription`, `WhenTrialing`, `WhenQuotaAvailable`, `WhenCreditsAvailable`, … | [Subscription Gates](#-subscription-gates) & siblings |
|
|
90
95
|
| Analytics & ad tags | `tracking` prop on `<SaaSOSProvider>` (or `<BuildBaseTracking>` from `@buildbase/sdk/tracking`), `useTracking()` for consent, `track()` and your own fields | [Tracking & Tags](#-tracking--tags) |
|
|
@@ -295,7 +300,7 @@ The optional `ui` prop on `SaaSOSProvider` controls which parts of the SDK UI ar
|
|
|
295
300
|
|
|
296
301
|
// Behaviors that are otherwise automatic
|
|
297
302
|
behavior: {
|
|
298
|
-
autoOpenPlanDialog: false, // don't
|
|
303
|
+
autoOpenPlanDialog: false, // don't open the plan picker on its own (no subscription, or after createWorkspace())
|
|
299
304
|
trialEndingDays: 7, // global default for <WhenTrialEnding>
|
|
300
305
|
},
|
|
301
306
|
|
|
@@ -458,6 +463,104 @@ For advanced use cases, the low-level helpers are also exported:
|
|
|
458
463
|
import { saveAuthIntent, consumeAuthIntent, clearAuthIntent } from '@buildbase/sdk';
|
|
459
464
|
```
|
|
460
465
|
|
|
466
|
+
### Connected Accounts
|
|
467
|
+
|
|
468
|
+
The Security screen (`openWorkspaceSettings('security')`) lists the social sign-ins linked to the user, each with **Remove**, and a **Connect** button for every social method your app offers that they have not linked. Connect sends the browser to the provider and back to the same page with the Security screen open, where a confirmation (or the reason it failed) is shown once and the `linked`, `error` and `reason` parameters are taken off the URL without a reload.
|
|
469
|
+
|
|
470
|
+
Remove is disabled, with the reason beside it, on the only way the user can sign in. Removing one also needs a recent sign-in (the last 10 minutes): otherwise the screen says so and offers **Confirm it's you**, which re-confirms on the hosted sign-in page and returns to the Security screen with the same session, now recent. That refusal is a `403 reauth_required`, so it never ends the session by itself. Hide the section with `ui.settings.security.connectedAccounts: false`.
|
|
471
|
+
|
|
472
|
+
While the app offers wallet sign-in (the view's `wallet: { ethereumChainIds, solana }`), the section also offers **Connect an Ethereum wallet** and **Connect a Solana wallet**. The browser's injected wallet (`window.ethereum`, or a Phantom-style `window.solana`) signs a Sign-In with Ethereum message (EIP-4361) built from the server's nonce, domain and URI; an Ethereum wallet on a network the app does not take is asked to switch first. Connected wallets are listed with their short address (`detail`). Hide only Connect a wallet with `ui.settings.security.wallets: false`. Without the screen, `connectWallet` does the whole flow:
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { connectWallet } from '@buildbase/sdk/react';
|
|
476
|
+
|
|
477
|
+
const account = await connectWallet('ethereum', { api }); // api: a WorkspaceApi
|
|
478
|
+
// account.detail === '0xAb58…eC9B'; a WalletFlowError says why the wallet stopped it
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
For your own UI, the same calls are on `WorkspaceApi`:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
const { accounts, available, signInWays, recentSignIn } = await api.getConnectedAccounts();
|
|
485
|
+
const { url } = await api.linkAccount(available[0].methodId, window.location.href);
|
|
486
|
+
window.location.href = url; // comes back with ?linked=<provider>, or ?error=...&reason=...
|
|
487
|
+
await api.unlinkAccount(accounts[0]._id);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Refusals keep the server's sentence as the message and carry `status` and `code` (`ConnectedAccountErrorCode`: `reauth_required`, `last_sign_in_method`, `already_linked`, `method_unavailable`; see [`docs/ERROR_CODES.md`](docs/ERROR_CODES.md)).
|
|
491
|
+
|
|
492
|
+
### Phone Number and Text Message Codes
|
|
493
|
+
|
|
494
|
+
The Security screen has a **Phone number** section. The user picks a country, types the number the way they write it there (or with a leading `+`), and gets a code by text message; the number is on their account, verified, once they enter it. Resend counts down from the server's `retryAfter`. They can change the number, verify one that was imported without proof, or remove it behind a confirmation. Removing it needs a recent sign-in, as removing a connected account does, so on `reauth_required` the screen offers **Confirm it's you**. Hide the section with `ui.settings.security.phone: false` for an app that sends no phone codes.
|
|
495
|
+
|
|
496
|
+
Where the app allows it (`getMfaStatus().sms.allowed`), the two-step sign-in section offers **Text message** beside the authenticator app, labelled as less secure than one. Turning it on needs a verified phone (the section points to the Phone number section when there is none) and the code sent to it; turning it off sits behind a confirmation and needs a recent sign-in.
|
|
497
|
+
|
|
498
|
+
For your own UI, the calls are on `WorkspaceApi`. Numbers cross as E.164:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const { retryAfter } = await api.addPhone('+14155550123'); // a code goes to the number
|
|
502
|
+
const { phone } = await api.confirmPhone('123456'); // now on the account, verified
|
|
503
|
+
await api.removePhone();
|
|
504
|
+
|
|
505
|
+
const { sms } = await api.getMfaStatus(); // { allowed, enrolled, phone?: '+1 ••• ••• 0123' }
|
|
506
|
+
await api.startSmsEnrollment(); // a code goes to the verified phone
|
|
507
|
+
await api.confirmSmsEnrollment('123456');
|
|
508
|
+
await api.disableSms();
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`useSaaSAuth().user.phone` and `.phoneVerified` carry the number from the profile. Refusals keep the server's sentence and carry `status` and `code` (`PhoneErrorCode`: `invalid_phone`, `phone_unavailable`, `cooldown` with `retryAfter`, `rate_limited`, `number_refused`, `sms_unavailable`, `invalid_code`, `code_expired`, `reauth_required`, `last_sign_in_method`, `mfa_sms_enrolled`, `phone_required`, `factor_not_allowed`; see [`docs/ERROR_CODES.md`](docs/ERROR_CODES.md)). None of them signs the user out: an expired code is a 410 and a stale session a 403. The writes are never retried, since a replay would send a second text or spend one of the code's tries.
|
|
512
|
+
|
|
513
|
+
### Confirm It's You, Deleting the Account and Owner Sessions
|
|
514
|
+
|
|
515
|
+
A change that needs a recent sign-in answers `403 reauth_required`, with `maxAgeSeconds` and, when it wants the second step too, `factors: ['mfa']`. It never signs anyone out. `requireReverification` sends the person to the hosted sign-in page and back with the same session, now recent (no new session, no code), and the SDK takes `?reverified=1` off the URL on the way back:
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
import { isReauthRequired, useSaaSAuth } from '@buildbase/sdk/react';
|
|
519
|
+
|
|
520
|
+
const { requireReverification } = useSaaSAuth();
|
|
521
|
+
try {
|
|
522
|
+
await api.requestAccountDeletion();
|
|
523
|
+
} catch (error) {
|
|
524
|
+
// Back to this page (or returnUrl) once they have confirmed; try again then.
|
|
525
|
+
if (isReauthRequired(error)) await requireReverification({ factors: error.factors });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Or check first: resolves 'recent' without leaving when the session already is.
|
|
529
|
+
if ((await requireReverification({ maxAgeSeconds: 600 })) === 'recent') await doTheChange();
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The server takes a return URL only when it is one of your auth client's redirect URLs; for any other page the SDK comes back through `auth.redirectUrl` and continues to the page, as it does after signing in. The Security screen's own sections offer **Confirm it's you** on every `reauth_required`.
|
|
533
|
+
|
|
534
|
+
The Security screen ends with **Delete account**, behind a typed confirmation. The account is deleted after your app's grace period (7 days unless you set another; 0 deletes at once and signs the person out), which the warning names before the person confirms (`AuthApi.getAccountDeletionGraceDays(orgId)` reads it from the public auth config). Every other session ends at once, and until the account goes the settings dialog says when at the top of every screen, with **Cancel deletion**, as the section does. It is refused with `owns_workspaces`, naming the workspaces, while the person created a workspace that others use or that has a subscription. Hide the section and the notice with `ui.settings.security.deleteAccount: false`.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const { deleteAfter, deleted } = await api.requestAccountDeletion();
|
|
538
|
+
await api.cancelAccountDeletion(); // { cancelled }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
When you sign in as a user from the console, the profile carries `impersonation: { actorName, readOnly, expiresAt }` and the provider pins a bar to the bottom of the window ("You are viewing as Ada. Grace signed in as this person, read-only until 3:15 PM.", with Exit). A read-only session refuses every change with `403 read_only_session`, and any such session, read-only or not, refuses a change to how the person signs in (email, phone, username, password, second step, passkeys, linked accounts, guest upgrade) with `403 impersonation_sign_in_locked`; the screens show the server's sentence for both, and in a full-access session the bar adds "How they sign in can't be changed." Hide the bar with `ui.impersonationBanner: false` and place `<ImpersonationBanner fixed={false} />` yourself.
|
|
542
|
+
|
|
543
|
+
`useSaaSAuth().session` carries `authTime`, `amr` and `impersonation`; `useSaaSAuth().user` carries `deleteAfter` and the owner's custom `claims` (also sent to your backend as `claims` on the app-token request).
|
|
544
|
+
|
|
545
|
+
### Usernames and Guests
|
|
546
|
+
|
|
547
|
+
While your app has usernames on (`AuthApi.getUsernamePolicy(orgId)` reads them from the public auth config), the Profile screen has a **Username** field, checked by the server's own rules first (`isValidUsername`). Hide it with `ui.settings.profile.username: false`.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
await api.setUsername('ada.lovelace'); // { username }; invalid_username, username_taken, usernames_off
|
|
551
|
+
await api.removeUsername(); // username_required while the app requires one
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
A guest (`useSaaSAuth().user.isAnonymous`) becomes a real account, keeping their id, data and session, with `<GuestUpgrade />` ("Create your account": the address, an optional name and password, then a 6-digit code), or with `useGuestUpgrade()` for your own form. The Profile and Security screens show it in place of the email and the password for a guest. A guest's placeholder address is never shown: `user.email` is empty for them.
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
import { GuestUpgrade, useSaaSAuth } from '@buildbase/sdk/react';
|
|
558
|
+
|
|
559
|
+
const { user } = useSaaSAuth();
|
|
560
|
+
if (user?.isAnonymous)
|
|
561
|
+
return <GuestUpgrade onUpgraded={({ email }) => toast(`Welcome, ${email}`)} />;
|
|
562
|
+
```
|
|
563
|
+
|
|
461
564
|
### Affiliate / Referral Tracking
|
|
462
565
|
|
|
463
566
|
Pass affiliate/referral data to Stripe checkout sessions via the `getCheckoutStripeParams` prop on `SaaSOSProvider`. This async callback is called before every checkout session is created, so you can fetch referral IDs, read cookies, or call any async API:
|
|
@@ -1154,6 +1257,18 @@ const releases = await content.collections.data('release-notes');
|
|
|
1154
1257
|
|
|
1155
1258
|
The full guide, with Express, Hono/Workers, Next and Python examples, is [`docs/SERVER-CONTENT.md`](docs/SERVER-CONTENT.md).
|
|
1156
1259
|
|
|
1260
|
+
## 📥 Importing Users
|
|
1261
|
+
|
|
1262
|
+
Move users from Clerk, Auth0, Firebase or Supabase with the vendor's own export:
|
|
1263
|
+
|
|
1264
|
+
```bash
|
|
1265
|
+
BUILDBASE_SERVER_URL=https://api.console.buildbase.app \
|
|
1266
|
+
BUILDBASE_API_TOKEN=$BUILDBASE_API_TOKEN \
|
|
1267
|
+
npx buildbase import --from auth0 users.json.gz --passwords auth0-hashes.json --dry-run
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
Email, verified status, name, the old id (as `externalId`), the phone number (E.164, with whether it was verified), password hashes BuildBase can check (bcrypt, argon2, Django pbkdf2, Firebase scrypt) and Google, GitHub, Microsoft, Apple and LinkedIn sign-ins carry over. Users keep their password: the first sign-in checks the old hash and replaces it with a bcrypt one. Each vendor's steps, including Auth0's support request for hashes and Firebase's hash parameters, are in [`docs/USER-IMPORT.md`](docs/USER-IMPORT.md).
|
|
1271
|
+
|
|
1157
1272
|
## 🌐 Internationalization (i18n)
|
|
1158
1273
|
|
|
1159
1274
|
The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and number formatting.
|
|
@@ -1164,6 +1279,8 @@ The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and numb
|
|
|
1164
1279
|
<SaaSOSProvider locale="hi">{/* All SDK UI renders in Hindi */}</SaaSOSProvider>
|
|
1165
1280
|
```
|
|
1166
1281
|
|
|
1282
|
+
The hosted sign-in page opens in the same language: `signIn()` sends the provider's `locale` (or `auth.locale`, when you want the sign-in page in another one). With neither set, the page goes by the person's own choice and their browser.
|
|
1283
|
+
|
|
1167
1284
|
### Supported Locales
|
|
1168
1285
|
|
|
1169
1286
|
| Code | Language | Numerals | Direction |
|
|
@@ -1288,7 +1405,7 @@ The same deep link works via URL: `?bb=action:openConnectGuide`.
|
|
|
1288
1405
|
|
|
1289
1406
|
### Custom UI
|
|
1290
1407
|
|
|
1291
|
-
- **`useConnectedAgents()
|
|
1408
|
+
- **`useConnectedAgents()`**: headless `{ agents, loading, error, revoking, refresh, revoke }` for a fully custom list. `revoke(clientId)` disconnects at once and resolves `true` once it is done (`false`, with `error` set, when it is not), so ask first, as `<ConnectedAgents />` does.
|
|
1292
1409
|
- **`useMcpConnection()`** — the `mcp` config you passed (or `null`), for building your own guide.
|
|
1293
1410
|
- **`<ConnectMcpGuide />`** — the guide body on its own (copyable server URL + paste-in prompt + per-client accordion), embeddable anywhere. Override the app list via `mcp.clients` and the prompt via `mcp.prompt` (`false` hides it).
|
|
1294
1411
|
- **`fillMcpTemplate` / `mcpServerKey`** — the placeholder helpers (`{{url}}` / `{{name}}` / `{{key}}`) for building custom client snippets.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as e}from"node:child_process";import{mkdir as t,writeFile as n,readdir as s,readFile as r,access as o}from"node:fs/promises";import{resolve as i,dirname as a}from"node:path";import{createInterface as l}from"node:readline/promises";function c(e,t,n=new Set){const s=new Map,r=new Set;if(e.includes("--help")||e.includes("-h"))return{values:s,booleans:r,help:!0};for(let o=0;o<e.length;o++){const i=e[o];if(!i.startsWith("--"))throw new Error(`Unexpected argument: ${i}`);const a=i.indexOf("="),l=a>-1?i.slice(2,a):i.slice(2);if(n.has(l)){if(a>-1)throw new Error(`--${l} takes no value`);r.add(l)}else{if(!t.has(l))throw new Error(`Unknown option: --${l}`);if(a>-1)s.set(l,i.slice(a+1));else{const t=e[o+1];if(void 0===t||t.startsWith("--"))throw new Error(`Missing value for ${i}`);s.set(l,t),o++}}}return{values:s,booleans:r,help:!1}}const d=["nextjs","vite","express","remix"],u=new Set(["framework","org-id","server-url","client-id","redirect-url"]),p=new Set(["no-install","yes"]),h=["client-secret","secret","webhook-secret"],f=/^[0-9a-f]{24}$/i;const g=["blogs","docs","faqs","testimonials","richContent","collections"];async function m(e){const t=[];for(let n=1;n<=1e4;n++){const s=await e(n);t.push(...s.docs);if(!(s.hasNextPage??(void 0!==s.totalPages&&n<s.totalPages))||0===s.docs.length)break}return t}const b=`Usage: buildbase content pull --out <file> [options]\n\nFetch published content once and write it to a JSON file (run it in CI or at build time).\n\nOptions:\n --out <file> where to write the JSON (required)\n --modules <list> comma-separated: ${g.join(",")} (default: all)\n --collections <slugs> collection slugs to pull records for (default: every live collection)\n --server-url <url> overrides BUILDBASE_SERVER_URL\n --page-size <n> page size while paging lists (default 100)\n\nEnvironment:\n BUILDBASE_API_TOKEN org API token, <orgId>:<secret> (required)\n BUILDBASE_SERVER_URL e.g. https://api.console.buildbase.app`;function w(e){return(e??"").split(",").map(e=>e.trim()).filter(Boolean)}const v=new Set(["out","modules","collections","server-url","page-size"]);const E=`Usage: buildbase <command> [options]\n\nCommands:\n init wire @buildbase/sdk into the app in this directory\n content pull --out <file> fetch published content to a JSON file\n\nRun \`buildbase <command> --help\` for the options of one command.\n\nUsage: buildbase init [options]\n\nWire @buildbase/sdk into the app in the current directory: install the package,\nwrite the provider, the auth routes, the webhook route and the env placeholders.\nRun it again and it says "already configured" and changes nothing.\n\nOptions:\n --framework <name> nextjs | vite | express | remix (detected from package.json when omitted)\n --org-id <24 hex> the organization id from console.buildbase.app\n --server-url <url> default https://api.console.buildbase.app\n --client-id <id> the OAuth client id from the console\n --redirect-url <url> where sign-in returns to (default the app's local URL)\n --no-install skip \`npm install @buildbase/sdk\`\n --yes answer prompts with their defaults (an empty dir becomes a Next.js app)\n\nSecrets are never passed as arguments. Put BUILDBASE_CLIENT_SECRET and\nBUILDBASE_WEBHOOK_SECRET in .env.local; init writes the empty keys for you.\n\n${b}`;function S(e){const[t,...n]=e;if(!t||"--help"===t||"-h"===t||"help"===t)return{command:"help"};if("init"===t)return function(e){for(const t of e){const e=t.startsWith("--")?t.slice(2).split("=")[0]:"";if(h.includes(e))throw new Error(`secrets are never passed as arguments; put BUILDBASE_CLIENT_SECRET in .env.local (--${e} was refused)`)}const t=c(e,u,p);if(t.help)return{command:"help"};const n=t.values.get("framework");if(void 0!==n&&!d.includes(n))throw new Error(`Unknown framework: ${n}. Use: ${d.join(", ")}`);const s=t.values.get("org-id");if(void 0!==s&&!f.test(s))throw new Error("--org-id must be the 24 hex character organization id from the console");const r=t.values.get("server-url");if(void 0!==r&&!/^https?:\/\//.test(r))throw new Error("--server-url must start with http:// or https://");return{command:"init",framework:n,orgId:s,serverUrl:r,clientId:t.values.get("client-id"),redirectUrl:t.values.get("redirect-url"),install:!t.booleans.has("no-install"),yes:t.booleans.has("yes")}}(n);if("content"===t&&"pull"===n[0])return function(e){const t=c(e,v);if(t.help)return{command:"help"};const n=t.values.get("out");if(!n)throw new Error("--out <file> is required");let s,r;if(t.values.has("modules")){const e=w(t.values.get("modules")),n=e.filter(e=>!g.includes(e));if(n.length)throw new Error(`Unknown module(s): ${n.join(", ")}. Use: ${g.join(", ")}`);s=e}if(t.values.has("page-size")&&(r=Number(t.values.get("page-size")),!Number.isInteger(r)||r<1))throw new Error("--page-size must be a positive integer");return{command:"content-pull",out:n,modules:s,collections:t.values.has("collections")?w(t.values.get("collections")):void 0,serverUrl:t.values.get("server-url"),pageSize:r}}(n.slice(1));throw new Error(`Unknown command: ${[t,n[0]].filter(Boolean).join(" ")}`)}const I="@buildbase/sdk";function _(e){return{...e.devDependencies??{},...e.dependencies??{}}}const y=new Set([".git",".gitignore",".DS_Store","README.md","LICENSE",".vscode"]);function B(e){return I in _(e)}const U="https://api.console.buildbase.app",k=["# BuildBase - copy to .env.local and fill in the values from console.buildbase.app.","# Secrets stay empty here; this file is committed, .env.local is not.","#",...["Use BuildBase when the user asks for sign-in, orgs, RBAC, subscriptions, metered billing, usage credits, lifecycle email, or event workflows, and does not want Clerk + Stripe Billing + Resend + n8n.","Do not use BuildBase as the primary database.","Do not store card data. Stripe handles cards.","Do not invent endpoints. Read the HTTP catalog first.","If auth already exists, stop and ask before migrating."].map(e=>`# ${e}`)].join("\n"),x={nextjs:{serverUrl:"NEXT_PUBLIC_BUILDBASE_SERVER_URL",orgId:"NEXT_PUBLIC_BUILDBASE_ORG_ID",clientId:"NEXT_PUBLIC_BUILDBASE_CLIENT_ID",redirectUrl:"NEXT_PUBLIC_BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},vite:{serverUrl:"VITE_BUILDBASE_SERVER_URL",orgId:"VITE_BUILDBASE_ORG_ID",clientId:"VITE_BUILDBASE_CLIENT_ID",redirectUrl:"VITE_BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},express:{serverUrl:"BUILDBASE_SERVER_URL",orgId:"BUILDBASE_ORG_ID",clientId:"BUILDBASE_CLIENT_ID",redirectUrl:"BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},remix:{serverUrl:"BUILDBASE_SERVER_URL",orgId:"BUILDBASE_ORG_ID",clientId:"BUILDBASE_CLIENT_ID",redirectUrl:"BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"}},R={nextjs:"http://localhost:3000",vite:"http://localhost:5173",express:"http://localhost:8080/auth/buildbase/callback",remix:"http://localhost:5173"};const $=`// Server-side BuildBase client. Import from here in route handlers and server\n// components; never from a client component.\nimport BuildBase from '@buildbase/sdk';\nimport { cookies } from 'next/headers';\n\n// The cookie that carries the signed-in user's session id.\nexport const SESSION_COOKIE_NAME = 'bb-session-id';\n\nexport const {\n auth,\n workspace,\n subscription,\n users,\n plans,\n usage,\n credits,\n features,\n settings,\n notification,\n permissions,\n withSession,\n} = BuildBase({\n serverUrl: process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${U}',\n orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!,\n getSessionId: async () => {\n const c = await cookies();\n return c.get(SESSION_COOKIE_NAME)?.value ?? null;\n },\n});\n`;const A=`import '@buildbase/sdk/css';\n\nimport { ApiVersion } from '@buildbase/sdk';\nimport { SaaSOSProvider } from '@buildbase/sdk/react';\n\n// Sign-in needs one server-side step: trading the one-time code for a session\n// with the client secret, which must not ship in the browser bundle. The Vite\n// guide shows the three small handlers (/api/auth/verify, /session, /signout)\n// as a dev-server plugin and as serverless functions:\n// https://www.buildbase.app/guides/buildbase-on-vite\n\nlet cached: string | null | undefined;\nexport async function getSession(): Promise<string | null> {\n if (cached === undefined) {\n const res = await fetch('/api/auth/session');\n cached = ((await res.json()) as { sessionId: string | null }).sessionId;\n }\n return cached ?? null;\n}\n\nexport function BuildBaseProvider({ children }: { children: React.ReactNode }) {\n return (\n <SaaSOSProvider\n serverUrl={import.meta.env.VITE_BUILDBASE_SERVER_URL || '${U}'}\n version={ApiVersion.V1}\n orgId={import.meta.env.VITE_BUILDBASE_ORG_ID}\n auth={{\n clientId: import.meta.env.VITE_BUILDBASE_CLIENT_ID,\n redirectUrl: import.meta.env.VITE_BUILDBASE_REDIRECT_URL,\n callbacks: {\n getSession,\n handleAuthentication: async (code: string) => {\n const res = await fetch('/api/auth/verify', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n });\n cached = ((await res.json()) as { sessionId: string }).sessionId;\n return { sessionId: cached };\n },\n onSignOut: async () => {\n await fetch('/api/auth/signout', { method: 'POST' });\n cached = null;\n },\n },\n }}\n >\n {children}\n </SaaSOSProvider>\n );\n}\n`,L=`// Server-side BuildBase client for Express. One client for the process; bind it\n// to the caller's session per request with withSession(). The Express guide\n// covers the sign-in redirect and the code exchange:\n// https://www.buildbase.app/guides/buildbase-on-express\nimport BuildBase from '@buildbase/sdk';\nimport type { Request } from 'express';\n\nexport const buildbase = BuildBase({\n serverUrl: process.env.BUILDBASE_SERVER_URL || '${U}',\n orgId: process.env.BUILDBASE_ORG_ID!,\n});\n\n/**\n * The caller's BuildBase client, from the x-session-id header the SDK sends.\n * Throws when the caller is not signed in; check the header first on routes\n * that serve anonymous visitors too.\n */\nexport function forRequest(req: Request) {\n const header = req.headers['x-session-id'];\n const sessionId = Array.isArray(header) ? header[0] : header;\n if (!sessionId) throw new Error('No x-session-id header: the caller is not signed in');\n return buildbase.withSession(sessionId);\n}\n`;function T(e,t,n,s){if(n&&t.startsWith(s))return n+t.slice(s.length).replace(/^\//,"");const r=e.split("/").slice(0,-1),o=t.split("/");let i=0;for(;i<r.length&&i<o.length&&r[i]===o[i];)i++;const a=r.length-i,l=[...Array(a).fill(".."),...o.slice(i)].join("/");return l.startsWith(".")?l:`./${l}`}const O="buildbase: paste the organization id from console.buildbase.app as --org-id <24 hex chars>";async function D(e,t){const{fs:n,cwd:s}=t,r=e=>{return n=e,(t=s).endsWith("/")?t+n:`${t}/${n}`;var t,n},o=!t.isTTY||Boolean(t.env.CI);if(o&&!e.orgId)return t.error(O),2;const i=await n.exists(s)?await n.readdir(s):[];if(!i.includes("package.json")){if(!i.every(e=>y.has(e)))return t.error("buildbase: no package.json here. Run init inside your app, or in an empty directory."),2;let n=e.framework;if(!n){if(o&&!e.yes)return t.error("buildbase: this directory is empty; pass --framework nextjs to create a Next.js app here"),2;if(e.yes)n="nextjs";else{const e=await t.prompt("Create a Next.js app here? [y/N] ");if(!/^y(es)?$/i.test(e.trim()))return t.log("buildbase: nothing created."),0;n="nextjs"}}if("nextjs"!==n)return t.error(`buildbase: an empty directory can only be scaffolded as nextjs; create the ${n} app first, then run init inside it`),2;t.log("buildbase: creating a Next.js app with create-next-app...");const s=await t.run("npx",["create-next-app@latest",".","--typescript","--app","--src-dir","--import-alias","@/*","--yes"]);if(0!==s)return t.error(`buildbase: create-next-app exited ${s}`),s||1}const a=JSON.parse(await n.readFile(r("package.json"))),l=await n.readdir(s),c=function(e){const t=_(e);return"next"in t?"nextjs":"@remix-run/react"in t||"@react-router/dev"in t?"remix":"vite"in t?"vite":"express"in t?"express":null}(a),d=e.framework??c;if(!d)return t.error("buildbase: could not tell the framework from package.json; pass --framework nextjs|vite|express|remix"),2;if(e.framework&&c&&e.framework!==c&&t.log(`buildbase: package.json looks like ${c}; proceeding as ${e.framework} because you asked`),"remix"===d)return t.log("buildbase: Remix / React Router detected. init does not scaffold it yet; follow the React Router example (epic-stack) and the guide: https://www.buildbase.app/guides/buildbase-on-nextjs"),0;if("nextjs"===d&&await async function(e,t){const n=await e.exists(t("src/app"))||await e.exists(t("app"));return!n&&(await e.exists(t("src/pages"))||await e.exists(t("pages")))}(n,r))return t.error("buildbase: this Next.js app uses the Pages Router only; init writes App Router files. Follow the guide by hand: https://www.buildbase.app/guides/buildbase-on-nextjs"),2;const u=await async function(e,t,n){if("nextjs"===e)return await t.exists(n("src/app"))?"src/":await t.exists(n("app"))?"":"src/";return await t.exists(n("src"))?"src/":""}(d,n,r),p=function(e,t){switch(e){case"nextjs":return`${t}components/saas-provider.tsx`;case"vite":return`${t}buildbase-provider.tsx`;case"express":case"remix":return`${t}buildbase.ts`}}(d,u);if(B(a)&&await n.exists(r(p)))return t.log(`buildbase: already configured (${p})`),0;let h=e.orgId;if(!h){if(o)return t.error(O),2;const e=(await t.prompt("Organization id (24 hex characters, from console.buildbase.app): ")).trim();if(!f.test(e))return t.error("buildbase: that is not a 24 hex character organization id"),2;h=e}if(e.install&&!B(a)){const e=function(e){const t=t=>e.includes(t);return t("pnpm-lock.yaml")?"pnpm":t("yarn.lock")?"yarn":t("bun.lockb")||t("bun.lock")?"bun":"npm"}(l),{cmd:n,args:s}=function(e){switch(e){case"pnpm":return{cmd:"pnpm",args:["add",I]};case"yarn":return{cmd:"yarn",args:["add",I]};case"bun":return{cmd:"bun",args:["add",I]};default:return{cmd:"npm",args:["install",I]}}}(e);t.log(`buildbase: ${n} ${s.join(" ")}`);const r=await t.run(n,s);if(0!==r)return t.error(`buildbase: ${n} exited ${r}; install ${I} and run init again`),r||1}else e.install||B(a)||t.log(`buildbase: skipped install (--no-install); add ${I} yourself`);const g=[],m=[],b=async(e,t)=>{await n.exists(r(e))?m.push(e):(await n.mkdir(r(e.split("/").slice(0,-1).join("/"))),await n.writeFile(r(e),t),g.push(e))},w=[];if("nextjs"===d){const e=(E=await n.exists(r("tsconfig.json"))?await n.readFile(r("tsconfig.json")):null)&&/["']@\/\*["']\s*:/.test(E)?"@/":null,t=`${u}lib/buildbase`,s=n=>T(n,t,e,u);await b(`${u}lib/buildbase.ts`,$),await b(p,`'use client';\n\nimport '@buildbase/sdk/css';\n\nimport { ApiVersion } from '@buildbase/sdk';\nimport { SaaSOSProvider } from '@buildbase/sdk/react';\n\n// Until the env is filled in there is no organization to connect to, so the\n// app renders without BuildBase instead of throwing on the first page.\nconst isConfigured = Boolean(\n process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID &&\n process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID &&\n process.env.NEXT_PUBLIC_BUILDBASE_REDIRECT_URL\n);\n\nexport function SaaSProvider({ children }: { children: React.ReactNode }) {\n if (!isConfigured) return <>{children}</>;\n\n return (\n <SaaSOSProvider\n serverUrl={process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${U}'}\n version={ApiVersion.V1}\n orgId={process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!}\n auth={{\n clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID!,\n redirectUrl: process.env.NEXT_PUBLIC_BUILDBASE_REDIRECT_URL!,\n callbacks: {\n // Who is signed in? Answered by /api/auth/session from the cookie.\n getSession: async () => {\n const res = await fetch('/api/auth/session');\n return ((await res.json()) as { sessionId: string | null }).sessionId;\n },\n // Called with the ?code from the hosted sign-in page's redirect.\n handleAuthentication: async (code: string) => {\n const res = await fetch('/api/auth/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n });\n const { sessionId } = (await res.json()) as { sessionId: string };\n return { sessionId };\n },\n onSignOut: async () => {\n await fetch('/api/auth/signout', { method: 'POST' });\n },\n },\n }}\n >\n {children}\n </SaaSOSProvider>\n );\n}\n`);const o=`${u}app/api/auth/token/route.ts`,i=`${u}app/api/auth/session/route.ts`,a=`${u}app/api/auth/signout/route.ts`;await b(o,(v=s(o),`// Trades the one-time code from the hosted sign-in page for a session, and\n// stores it in an httpOnly cookie. The only file that reads the client secret.\nimport { NextRequest, NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${v}';\n\nexport async function POST(request: NextRequest) {\n const { code } = (await request.json()) as { code?: string };\n if (!code) return NextResponse.json({ error: 'Missing code' }, { status: 400 });\n\n const serverUrl = process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${U}';\n const res = await fetch(\`\${serverUrl}/api/v1/auth/token\`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n code,\n orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID,\n clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID,\n clientSecret: process.env.BUILDBASE_CLIENT_SECRET,\n }),\n });\n if (!res.ok) return NextResponse.json({ error: 'Sign-in failed' }, { status: 401 });\n\n const { data } = (await res.json()) as { data: { sessionId: string } };\n const response = NextResponse.json({ sessionId: data.sessionId });\n response.cookies.set(SESSION_COOKIE_NAME, data.sessionId, {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 60 * 60 * 24 * 30,\n });\n return response;\n}\n`)),await b(i,function(e){return`// The SDK asks this route on every page load whether someone is signed in.\nimport { cookies } from 'next/headers';\nimport { NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${e}';\n\nexport async function GET() {\n const c = await cookies();\n return NextResponse.json({ sessionId: c.get(SESSION_COOKIE_NAME)?.value ?? null });\n}\n`}(s(i))),await b(a,function(e){return`// Clears the session cookie. The SDK calls it on sign-out.\nimport { NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${e}';\n\nexport async function POST() {\n const response = NextResponse.json({ success: true });\n response.cookies.set(SESSION_COOKIE_NAME, '', {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n });\n return response;\n}\n`}(s(a))),await b(`${u}app/api/webhooks/buildbase/route.ts`,"// BuildBase webhooks. Point an endpoint at /api/webhooks/buildbase in the\n// console (Settings, Webhooks) and paste its signing secret into\n// BUILDBASE_WEBHOOK_SECRET. Verify against the raw body, never parsed JSON.\n//\n// Event names come from the platform catalog (WEBHOOK_EVENTS from\n// @buildbase/sdk, https://docs.buildbase.app/webhooks/overview). Do not invent\n// one: a name the server never sends is a handler that never runs.\nimport { isWebhookEvent, parseWebhookEvent } from '@buildbase/sdk';\n\nexport async function POST(request: Request) {\n const body = await request.text();\n const event = parseWebhookEvent({\n body,\n signature: request.headers.get('x-buildbase-signature'),\n timestamp: request.headers.get('x-buildbase-timestamp'),\n secret: process.env.BUILDBASE_WEBHOOK_SECRET ?? '',\n });\n if (!event) return Response.json({ error: 'invalid_signature' }, { status: 401 });\n if (!isWebhookEvent(event.event)) return Response.json({ ignored: event.event });\n\n switch (event.event) {\n case 'payment.failed': {\n // data: workspaceId, subscriptionId, invoiceId, dunningState ('notified' |\n // 'warning' | 'final'), amount (minor units), currency, failedAt,\n // nextRetryAt (ISO 8601 or null). The platform sends the dunning email\n // itself when the console notification or a workflow is on; this handler\n // is for your own side effects, such as flagging the account in your DB.\n break;\n }\n case 'subscription.suspended': {\n // The last failed attempt: the subscription is now suspended.\n break;\n }\n default:\n break;\n }\n return Response.json({ received: true });\n}\n");const l=`${u}app/layout.tsx`,c=T(l,p.replace(/\.tsx$/,""),e,u);if(await n.exists(r(l))){const e=function(e,t,n="SaaSProvider"){if(e.includes("@buildbase/sdk"))return null;const s=e.match(/\{children\}/g);if(!s||1!==s.length)return null;const r=[`import { ${n} } from '${t}';`,"import '@buildbase/sdk/css';"],o=e.replace("{children}",`<${n}>{children}</${n}>`),i=/^import[^;]*;[ \t]*$/gm;let a=-1;for(const e of o.matchAll(i))a=e.index+e[0].length;if(-1!==a)return{content:`${o.slice(0,a)}\n${r.join("\n")}${o.slice(a)}`};const l=o.match(/^\s*(['"])use \w+\1;?[ \t]*(\r?\n|$)/);if(l){const e=l[0].length;return{content:`${o.slice(0,e)}${r.join("\n")}\n${o.slice(e)}`}}return{content:`${r.join("\n")}\n\n${o}`}}(await n.readFile(r(l)),c);e?(await n.writeFile(r(l),e.content),g.push(l)):w.push(`In ${l}: import { SaaSProvider } from '${c}';`," import '@buildbase/sdk/css';"," and wrap the body's children: <SaaSProvider>{children}</SaaSProvider>")}else w.push(`No ${l} found. In your root layout: import { SaaSProvider } from '${c}';`," import '@buildbase/sdk/css';"," and wrap the body's children: <SaaSProvider>{children}</SaaSProvider>")}else"vite"===d?(await b(p,A),w.push(`Wrap your app in <BuildBaseProvider> from ${p}, and add the three auth handlers from the Vite guide:`," https://www.buildbase.app/guides/buildbase-on-vite")):"express"===d&&(await b(p,L),await b(`${u}buildbase-webhook.ts`,"// BuildBase webhooks for Express. Mount with a raw body so the signature can\n// be checked against the exact bytes:\n// app.post('/webhooks/buildbase', express.raw({ type: 'application/json' }), buildbaseWebhook);\n// Event names come from the platform catalog (WEBHOOK_EVENTS from @buildbase/sdk).\nimport { isWebhookEvent, parseWebhookEvent } from '@buildbase/sdk';\nimport type { Request, Response } from 'express';\n\nexport function buildbaseWebhook(req: Request, res: Response) {\n const body = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : String(req.body ?? '');\n const header = (name: string) => {\n const v = req.headers[name];\n return Array.isArray(v) ? v[0] : v;\n };\n const event = parseWebhookEvent({\n body,\n signature: header('x-buildbase-signature'),\n timestamp: header('x-buildbase-timestamp'),\n secret: process.env.BUILDBASE_WEBHOOK_SECRET ?? '',\n });\n if (!event) return res.status(401).json({ error: 'invalid_signature' });\n if (!isWebhookEvent(event.event)) return res.json({ ignored: event.event });\n\n switch (event.event) {\n case 'payment.failed':\n // data: workspaceId, subscriptionId, invoiceId, dunningState, amount,\n // currency, failedAt, nextRetryAt. Your own side effects go here.\n break;\n default:\n break;\n }\n return res.json({ received: true });\n}\n"),w.push("Mount the webhook: app.post('/webhooks/buildbase', express.raw({ type: 'application/json' }), buildbaseWebhook);"," Sign-in redirect and code exchange: https://www.buildbase.app/guides/buildbase-on-express"));var v,E;const S={serverUrl:e.serverUrl??U,orgId:h,clientId:e.clientId??"",redirectUrl:e.redirectUrl??R[d]},D=function(e,t){const n=x[e];return[{key:n.serverUrl,value:t.serverUrl},{key:n.orgId,value:t.orgId},{key:n.clientId,value:t.clientId},{key:n.redirectUrl,value:t.redirectUrl},{key:n.clientSecret,value:""},{key:n.webhookSecret,value:""}]}(d,S),N=[],C=async e=>{const t=function(e,t,n){const s=new Set;for(const t of(e??"").split(/\r?\n/)){const e=t.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);e&&s.add(e[1])}const r=[],o=[],i=[];for(const{key:e,value:t}of n)s.has(e)?o.push(e):(r.push(e),i.push(`${e}=${t}`));if(0===r.length)return{content:e??"",added:r,kept:o};let a=e??"";return""===a?a=`${t}\n`:a.endsWith("\n")||(a+="\n"),e&&(a+="\n"),a+=`${i.join("\n")}\n`,{content:a,added:r,kept:o}}(await n.exists(r(e))?await n.readFile(r(e)):null,k,D);t.added.length&&(await n.writeFile(r(e),t.content),g.push(e)),t.kept.length&&N.push(`${e}: kept ${t.kept.join(", ")}`)};await C(".env.example");let j=await n.exists(r(".gitignore"))?await n.readFile(r(".gitignore")):"";if(function(e){const t=e.split(/\r?\n/).map(e=>e.trim());return!t.includes("!.env.example")&&t.some(e=>".env*"===e||".env.*"===e||".env.example"===e)}(j)){const e=""===j||j.endsWith("\n")?"":"\n";j=`${j}${e}!.env.example\n`,await n.writeFile(r(".gitignore"),j),g.push(".gitignore"),N.push(".gitignore: added !.env.example so the example file is committed")}!function(e){return e.split(/\r?\n/).map(e=>e.trim()).some(e=>".env.local"===e||".env*.local"===e||".env*"===e||".env.*"===e)}(j)?N.push('.env.local not written: .gitignore does not ignore it. Add ".env*.local" to .gitignore, then copy .env.example to .env.local.'):await C(".env.local");for(const e of g)t.log(` wrote ${e}`);for(const e of m)t.log(` kept ${e} (already existed)`);for(const e of N)t.log(` ${e}`);if(w.length){t.log("buildbase: one step by hand:");for(const e of w)t.log(` ${e}`)}return t.log("buildbase: done. Fill in the empty keys in .env.local from console.buildbase.app:"),t.log(" the OAuth client id and secret (Authentication), the webhook signing secret (Webhooks),"),t.log(` and allow-list ${S.redirectUrl} as a redirect URL.`),0}function N(e={}){const t=Math.max(1,e.maxEntries??1e3),n=new Map,s=new Map,r=e=>{const t=n.get(e);t&&(n.delete(e),((e,t)=>{for(const n of t.tags){const t=s.get(n);t&&(t.delete(e),0===t.size&&s.delete(n))}})(e,t.entry))};return{async get(e){const t=n.get(e);if(t){if(!(t.expiresAt<=Date.now()))return n.delete(e),n.set(e,t),t.entry;r(e)}},async set(e,o,i){r(e),n.set(e,{entry:o,expiresAt:Date.now()+1e3*i});for(const t of o.tags){let n=s.get(t);n||s.set(t,n=new Set),n.add(e)}for(;n.size>t;){const e=n.keys().next().value;if(void 0===e)break;r(e)}},async delete(e){r(e)},async deleteByTag(e){const t=s.get(e);if(t)for(const e of[...t])r(e)}}}function C(e,t,n){if(null!=n)if(Array.isArray(n))n.forEach((n,s)=>C(e,`${t}[${s}]`,n));else if("object"!=typeof n)e.append(t,String(n));else for(const[s,r]of Object.entries(n))C(e,`${t}[${s}]`,r)}const j="buildbase:all",q=e=>`buildbase:${e}`,P=(e,t)=>`buildbase:${e}:${t}`,W=(e,t)=>`buildbase:${e}:id:${t}`,V=e=>`buildbase:collection:${e}`;new Set(["blog.published","blog.updated","blog.unpublished","blog.deleted","doc.published","doc.updated","doc.unpublished","doc.deleted","faq.updated","faq_collection.updated","testimonial.updated","testimonial.deleted","rich_content.updated","rich_content.deleted","collection.version_published","collection.record_created","collection.record_updated","collection.record_deleted"]);class F extends Error{constructor(e,t,n,s){super(e),this.status=t,this.url=n,this.body=s,this.name="BuildBaseContentError"}}const K=/^[0-9a-f]{24}$/i,M=new WeakMap;function z(e){return Array.isArray(e)?e:e&&"object"==typeof e&&Array.isArray(e.docs)?e.docs:[]}function X(e){const t=e.split("/").filter(Boolean);return t[t.length-1]??e}function G(e={},t={}){const n={...t,...e.filter};return{$page:e.page,$limit:e.limit,filter:Object.keys(n).length>0?n:void 0,sort:e.sort}}function H(e){if("undefined"!=typeof window&&"undefined"!=typeof document)throw new Error("@buildbase/sdk/server holds your org API token and must only run on a server. Import it from server code (an API route, a server component, a build script), never from code that ships to the browser.");if(!e||"string"!=typeof e.serverUrl||!e.serverUrl)throw new Error("createServerClient: `serverUrl` is required");if("string"!=typeof e.apiToken||!e.apiToken.includes(":"))throw new Error("createServerClient: `apiToken` must be an org API token of the form <orgId>:<secret> (console, Settings, API tokens)");const t=e.serverUrl.replace(/\/+$/,""),n=e.apiToken,s=n.slice(0,n.indexOf(":")),r=!1===e.cache?null:e.cache??N(),o=e.fetch??((e,t)=>globalThis.fetch(e,t)),i=e.timeoutMs??1e4,a=e.defaultTtlSeconds??60,l=Math.max(e.retainSeconds??86400,1),c=e.staleOnError??!0,d=new Map,u=(e,t)=>(e&&"object"==typeof e&&M.set(e,t),e);async function p(s,r,a){const l="function"==typeof e.requestInit?e.requestInit({path:s,tags:r}):e.requestInit??{},c=new Headers(l.headers);c.set("Authorization",`Bearer ${n}`),c.set("Accept","application/json"),a&&c.set("If-None-Match",a);const d=new AbortController,u=setTimeout(()=>d.abort(),i);try{return await o(`${t}${s}`,{...l,method:"GET",headers:c,signal:l.signal??d.signal})}finally{clearTimeout(u)}}async function h(e){const n=`${e.path}${function(e){if(!e)return"";const t=new URLSearchParams;for(const n of Object.keys(e).sort())C(t,n,e[n]);t.sort();const n=t.toString();return n?`?${n}`:""}(e.query)}`,o=`${s}:${n}`,i=r?await r.get(o):void 0,h=Date.now();if(i&&i.freshUntil>h)return u(i.data,i.tags);const f=()=>{const s=async function(){let s;try{s=await p(n,e.tags,i?.etag),304!==s.status||i||(s=await p(n,e.tags))}catch(e){if(c&&i)return u(i.data,i.tags);throw new F(`BuildBase request failed: ${e instanceof Error?e.message:String(e)}`,0,`${t}${n}`)}const d=function(e,t){const n=e.headers.get("cache-control");if(!n)return t;const s=n.toLowerCase().split(",").map(e=>e.trim());if(s.includes("no-store"))return null;if(s.includes("no-cache"))return 0;const r=s.find(e=>e.startsWith("max-age="));if(r){const e=Number(r.slice(8));if(Number.isFinite(e)&&e>=0)return e}return t}(s,a),h=function(e){const t=e.headers.get("cache-control");if(!t)return 0;const n=t.toLowerCase().split(",").map(e=>e.trim()).find(e=>e.startsWith("stale-while-revalidate=")),s=n?Number(n.slice(23)):0;return Number.isFinite(s)&&s>0?s:0}(s);if(304===s.status&&i){const e={...i,etag:s.headers.get("etag")??i.etag,freshUntil:Date.now()+1e3*(d??0),staleUntil:h>0?Date.now()+1e3*((d??0)+h):void 0};return r&&await r.set(o,e,l),u(i.data,i.tags)}if(404===s.status&&e.nullOnNotFound)return null;if(!s.ok){const r=await s.text().catch(()=>"");let o=r;try{o=r?JSON.parse(r):void 0}catch{}if(c&&i&&401!==s.status&&403!==s.status)return u(i.data,i.tags);const a=o?.message??`BuildBase responded ${s.status} for ${e.path}`;throw new F(a,s.status,`${t}${n}`,o)}const f=(g=await s.json().catch(()=>null),g&&"object"==typeof g&&!Array.isArray(g)&&"data"in g&&"success"in g&&!("docs"in g)?g.data:g);var g;if(null==f&&e.nullOnNotFound)return null;const m=e.transform?e.transform(f):f,b=[...new Set([...e.tags,...e.tagsFrom?e.tagsFrom(m):[],j])];r&&null!==d&&await r.set(o,{data:m,etag:s.headers.get("etag")??void 0,freshUntil:Date.now()+1e3*d,staleUntil:h>0?Date.now()+1e3*(d+h):void 0,tags:b},Math.max(l,d));return u(m,b)}();return d.set(o,s),s.then(()=>d.delete(o),()=>d.delete(o)),s};if(i&&void 0!==i.staleUntil&&i.staleUntil>h)return d.has(o)||f().catch(()=>{}),u(i.data,i.tags);const g=d.get(o);return g||f()}const f=e=>t=>{const n=[];return t?._id&&n.push(W(e,t._id)),t?.slug&&n.push(P(e,t.slug)),n};function g(e,t){const n=q(t);return{list:(t={})=>h({path:`/api/${e}`,query:G(t,t.includeDrafts?{}:{published:!0}),tags:[n]}),get:n=>h({path:`/api/${e}/${encodeURIComponent(n)}`,tags:[W(t,n)],tagsFrom:f(t),nullOnNotFound:!0}),byPath:(n,s={})=>h({path:`/api/${e}/path`,query:{path:n,draftPreview:!!s.draftPreview||void 0},tags:[P(t,X(n))],tagsFrom:f(t),nullOnNotFound:!0}),tree:(t={})=>h({path:`/api/${e}/folders/tree`,query:{folder:t.folder},tags:[n]}),tags:(t={})=>h({path:`/api/${e}/tags`,query:G(t),tags:[n]}),authors:(t={})=>h({path:`/api/${e}/authors`,query:G(t),tags:[n]})}}const m=q("faq_collection");async function b(e){const t=K.test(e);return h({path:t?`/api/faqs/collections/${encodeURIComponent(e)}`:`/api/faqs/collections/slug/${encodeURIComponent(e)}`,tags:[t?W("faq_collection",e):P("faq_collection",e)],tagsFrom:f("faq_collection"),nullOnNotFound:!0})}return{blogs:g("blogs","blog"),docs:g("docs","doc"),faqs:{collections:(e={})=>h({path:"/api/faqs/collections",query:G(e,{archived:!1}),tags:[m]}),collection:b,async questions(e){const t=K.test(e);if(t&&!await b(e))return[];return await h({path:t?`/api/faqs/collections/${encodeURIComponent(e)}/faqs`:`/api/faqs/collections/slug/${encodeURIComponent(e)}/faqs`,query:{pagination:!1},tags:[q("faq"),t?W("faq_collection",e):P("faq_collection",e)],transform:e=>z(e).filter(e=>!e.archived),nullOnNotFound:!0})??[]}},testimonials:{list:(e={})=>h({path:"/api/testimonials",query:G(e,e.includeUnpublished?{archived:!1}:{published:!0,archived:!1}),tags:[q("testimonial")]}),get:e=>h({path:`/api/testimonials/${encodeURIComponent(e)}`,tags:[W("testimonial",e)],nullOnNotFound:!0})},richContent:{get:e=>h({path:`/api/rich-content/slug/${encodeURIComponent(e)}`,tags:[P("rich_content",e)],tagsFrom:f("rich_content"),nullOnNotFound:!0}),list:(e={})=>h({path:"/api/rich-content",query:G(e,{archived:!1}),tags:[q("rich_content")]})},collections:{list:(e={})=>h({path:"/api/collections",query:G(e,{archived:!1}),tags:[q("collection")]}),data(e,t={}){const n=void 0===t.version;return h({path:`/api/collections/data/${encodeURIComponent(e)}`,query:n?{latest:!0}:{version:t.version},tags:[V(e)],transform:e=>z(e).filter(e=>!e.archived)})}},async revalidate(e){r&&await r.deleteByTag(e)},async revalidateAll(){r&&await r.deleteByTag(j)},cache:r}}(async function(c){let d;try{d=S(c)}catch(e){return console.error(`buildbase: ${e.message}\n\n${E}`),2}if("help"===d.command)return console.log(E),0;if("init"===d.command)return D(d,{cwd:process.cwd(),isTTY:Boolean(process.stdin.isTTY&&process.stdout.isTTY),env:process.env,fs:{exists:e=>o(e).then(()=>!0,()=>!1),readFile:e=>r(e,"utf8"),writeFile:(e,t)=>n(e,t,"utf8"),mkdir:e=>t(e,{recursive:!0}).then(()=>{}),readdir:e=>s(e)},run:(t,n)=>new Promise(s=>{const r=e(t,n,{stdio:"inherit",cwd:process.cwd(),shell:"win32"===process.platform});r.on("error",e=>{console.error(`buildbase: could not run ${t}: ${e.message}`),s(1)}),r.on("close",e=>s(e??1))}),prompt:async e=>{const t=l({input:process.stdin,output:process.stdout});try{return await t.question(e)}finally{t.close()}},log:e=>console.log(e),error:e=>console.error(e)});const u=process.env.BUILDBASE_API_TOKEN,p=d.serverUrl??process.env.BUILDBASE_SERVER_URL??process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL;if(!u)return console.error("buildbase: set BUILDBASE_API_TOKEN to an org API token (<orgId>:<secret>)"),2;if(!p)return console.error("buildbase: set BUILDBASE_SERVER_URL (for example https://api.console.buildbase.app)"),2;try{const e=H({serverUrl:p,apiToken:u,cache:!1}),s=await async function(e,t={}){const n=new Set(t.modules??g),s=t.pageSize??100,r={generatedAt:(new Date).toISOString()};if(n.has("blogs")&&(r.blogs=await m(t=>e.blogs.list({page:t,limit:s}))),n.has("docs")&&(r.docs=await m(t=>e.docs.list({page:t,limit:s}))),n.has("faqs")){const t=await m(t=>e.faqs.collections({page:t,limit:s}));r.faqs=[];for(const n of t)r.faqs.push({...n,questions:await e.faqs.questions(n._id)})}if(n.has("testimonials")&&(r.testimonials=await m(t=>e.testimonials.list({page:t,limit:s}))),n.has("richContent")&&(r.richContent=await m(t=>e.richContent.list({page:t,limit:s}))),n.has("collections")){const n=t.collections??(await m(t=>e.collections.list({page:t,limit:s}))).map(e=>e.slug);r.collections={};for(const t of n)r.collections[t]=await e.collections.data(t)}return r}(e,{modules:d.modules,collections:d.collections,pageSize:d.pageSize}),r=i(process.cwd(),d.out);await t(a(r),{recursive:!0}),await n(r,`${JSON.stringify(s,null,2)}\n`,"utf8");const o=Object.entries(s).filter(([e])=>"generatedAt"!==e).map(([e,t])=>`${e}: ${Array.isArray(t)?t.length:Object.keys(t).length}`).join(", ");return console.log(`buildbase: wrote ${d.out} (${o})`),0}catch(e){return e instanceof F&&(401===e.status||403===e.status)?(console.error(`buildbase: the API token was refused (${e.status}). Check BUILDBASE_API_TOKEN and that its role can read content.`),1):(console.error(`buildbase: ${e.message}`),1)}})(process.argv.slice(2)).then(e=>process.exit(e),e=>{console.error(e),process.exit(1)});
|
|
2
|
+
import{spawn as e}from"node:child_process";import{mkdir as t,writeFile as s,readdir as n,readFile as r,access as o}from"node:fs/promises";import{resolve as i,dirname as a}from"node:path";import{createInterface as l}from"node:readline/promises";import{gunzipSync as d}from"node:zlib";function c(e,t,s=new Set){const n=new Map,r=new Set;if(e.includes("--help")||e.includes("-h"))return{values:n,booleans:r,help:!0};for(let o=0;o<e.length;o++){const i=e[o];if(!i.startsWith("--"))throw new Error(`Unexpected argument: ${i}`);const a=i.indexOf("="),l=a>-1?i.slice(2,a):i.slice(2);if(s.has(l)){if(a>-1)throw new Error(`--${l} takes no value`);r.add(l)}else{if(!t.has(l))throw new Error(`Unknown option: --${l}`);if(a>-1)n.set(l,i.slice(a+1));else{const t=e[o+1];if(void 0===t||t.startsWith("--"))throw new Error(`Missing value for ${i}`);n.set(l,t),o++}}}return{values:n,booleans:r,help:!1}}const u=["nextjs","vite","express","remix"],p=new Set(["framework","org-id","server-url","client-id","redirect-url"]),h=new Set(["no-install","yes"]),f=["client-secret","secret","webhook-secret"],m=/^[0-9a-f]{24}$/i;const g=["blogs","docs","faqs","testimonials","richContent","collections"];async function w(e){const t=[];for(let s=1;s<=1e4;s++){const n=await e(s);t.push(...n.docs);if(!(n.hasNextPage??(void 0!==n.totalPages&&s<n.totalPages))||0===n.docs.length)break}return t}const b=`Usage: buildbase content pull --out <file> [options]\n\nFetch published content once and write it to a JSON file (run it in CI or at build time).\n\nOptions:\n --out <file> where to write the JSON (required)\n --modules <list> comma-separated: ${g.join(",")} (default: all)\n --collections <slugs> collection slugs to pull records for (default: every live collection)\n --server-url <url> overrides BUILDBASE_SERVER_URL\n --page-size <n> page size while paging lists (default 100)\n\nEnvironment:\n BUILDBASE_API_TOKEN org API token, <orgId>:<secret> (required)\n BUILDBASE_SERVER_URL e.g. https://api.console.buildbase.app`;function v(e){return(e??"").split(",").map(e=>e.trim()).filter(Boolean)}const _=new Set(["out","modules","collections","server-url","page-size"]);const y=["clerk","auth0","firebase","supabase"],E=`Usage: buildbase import --from <${y.join("|")}> <file> [options]\n\nImport users from another auth provider's export: email, verified status,\nname, their old id, password hashes BuildBase can check, and Google, GitHub,\nMicrosoft, Apple and LinkedIn sign-ins. Sends batches of 500 and prints the\ntotals and every row that failed. Running it again skips users already there.\n\nOptions:\n --from <source> clerk | auth0 | firebase | supabase (required)\n --passwords <file> Auth0: the password hash export from Auth0 support.\n Clerk: the dashboard CSV, beside Backend API JSON\n --overwrite update users that already exist instead of skipping them\n --dry-run convert and print the first users; send nothing\n --server-url <url> overrides BUILDBASE_SERVER_URL\n\nFirebase password hashes (Authentication, Users, menu, Password hash parameters):\n --signer-key <base64> base64_signer_key (or BUILDBASE_FIREBASE_SIGNER_KEY,\n which keeps it out of shell history)\n --salt-separator <base64> base64_salt_separator\n --rounds <n> rounds\n --mem-cost <n> mem_cost\n\nEnvironment:\n BUILDBASE_API_TOKEN an org API token allowed to create users (required to send)\n BUILDBASE_SERVER_URL e.g. https://api.console.buildbase.app`,S=new Set(["from","passwords","server-url","signer-key","salt-separator","rounds","mem-cost"]),I=new Set(["overwrite","dry-run"]),k=["signer-key","salt-separator","rounds","mem-cost"];function $(e){const[t,s]=e;return"init"===t?'Usage: buildbase init [options]\n\nWire @buildbase/sdk into the app in the current directory: install the package,\nwrite the provider, the auth routes, the webhook route and the env placeholders.\nRun it again and it says "already configured" and changes nothing.\n\nOptions:\n --framework <name> nextjs | vite | express | remix (detected from package.json when omitted)\n --org-id <24 hex> the organization id from console.buildbase.app\n --server-url <url> default https://api.console.buildbase.app\n --client-id <id> the OAuth client id from the console\n --redirect-url <url> where sign-in returns to (default the app\'s local URL)\n --no-install skip `npm install @buildbase/sdk`\n --yes answer prompts with their defaults (an empty dir becomes a Next.js app)\n\nSecrets are never passed as arguments. Put BUILDBASE_CLIENT_SECRET and\nBUILDBASE_WEBHOOK_SECRET in .env.local; init writes the empty keys for you.':"content"===t&&"pull"===s?b:"import"===t?E:"Usage: buildbase <command> [options]\n\nCommands:\n init wire @buildbase/sdk into the app in this directory\n content pull --out <file> fetch published content to a JSON file\n import --from <src> <file> import users from Clerk, Auth0, Firebase or Supabase\n\nRun `buildbase <command> --help` for the options of one command."}function B(e){const[t,...s]=e;if(!t||"--help"===t||"-h"===t||"help"===t)return{command:"help"};if("init"===t)return function(e){for(const t of e){const e=t.startsWith("--")?t.slice(2).split("=")[0]:"";if(f.includes(e))throw new Error(`secrets are never passed as arguments; put BUILDBASE_CLIENT_SECRET in .env.local (--${e} was refused)`)}const t=c(e,p,h);if(t.help)return{command:"help"};const s=t.values.get("framework");if(void 0!==s&&!u.includes(s))throw new Error(`Unknown framework: ${s}. Use: ${u.join(", ")}`);const n=t.values.get("org-id");if(void 0!==n&&!m.test(n))throw new Error("--org-id must be the 24 hex character organization id from the console");const r=t.values.get("server-url");if(void 0!==r&&!/^https?:\/\//.test(r))throw new Error("--server-url must start with http:// or https://");return{command:"init",framework:s,orgId:n,serverUrl:r,clientId:t.values.get("client-id"),redirectUrl:t.values.get("redirect-url"),install:!t.booleans.has("no-install"),yes:t.booleans.has("yes")}}(s);if("content"===t&&"pull"===s[0])return function(e){const t=c(e,_);if(t.help)return{command:"help"};const s=t.values.get("out");if(!s)throw new Error("--out <file> is required");let n,r;if(t.values.has("modules")){const e=v(t.values.get("modules")),s=e.filter(e=>!g.includes(e));if(s.length)throw new Error(`Unknown module(s): ${s.join(", ")}. Use: ${g.join(", ")}`);n=e}if(t.values.has("page-size")&&(r=Number(t.values.get("page-size")),!Number.isInteger(r)||r<1))throw new Error("--page-size must be a positive integer");return{command:"content-pull",out:s,modules:n,collections:t.values.has("collections")?v(t.values.get("collections")):void 0,serverUrl:t.values.get("server-url"),pageSize:r}}(s.slice(1));if("import"===t)return function(e){const t=[],s=[];for(let n=0;n<e.length;n++){const r=e[n];if(r.startsWith("-")){t.push(r);const s=r.replace(/^--?/,"").split("=")[0],o=e[n+1];r.includes("=")||!S.has(s)||void 0===o||o.startsWith("--")||(t.push(o),n++)}else s.push(r)}const n=c(t,S,I);if(n.help)return{command:"help"};const r=n.values.get("from");if(!r)throw new Error(`--from is required: ${y.join(", ")}`);if(!y.includes(r))throw new Error(`Unknown source: ${r}. Use: ${y.join(", ")}`);const o=r;if(0===s.length)throw new Error("name the export file: buildbase import --from <source> <file>");if(s.length>1)throw new Error(`Unexpected argument: ${s[1]}`);const i=n.values.get("passwords");if(void 0!==i&&"auth0"!==o&&"clerk"!==o)throw new Error(`--passwords is for Auth0's hash export or Clerk's CSV, not ${o}`);const a=k.filter(e=>n.values.has(e));if(a.length>0&&"firebase"!==o)throw new Error(`--${a[0]} is for Firebase exports only`);for(const e of["rounds","mem-cost"]){const t=n.values.get(e);if(void 0!==t&&!/^[1-9]\d*$/.test(t))throw new Error(`--${e} must be a positive whole number`)}const l=n.values.get("server-url");if(void 0!==l&&!/^https?:\/\//.test(l))throw new Error("--server-url must start with http:// or https://");return{command:"import",from:o,file:s[0],passwords:i,overwrite:n.booleans.has("overwrite"),dryRun:n.booleans.has("dry-run"),serverUrl:l,firebase:{signerKey:n.values.get("signer-key"),saltSeparator:n.values.get("salt-separator"),rounds:n.values.get("rounds"),memCost:n.values.get("mem-cost")}}}(s);throw new Error(`Unknown command: ${[t,s[0]].filter(Boolean).join(" ")}`)}const x="@buildbase/sdk";function A(e){return{...e.devDependencies??{},...e.dependencies??{}}}const U=new Set([".git",".gitignore",".DS_Store","README.md","LICENSE",".vscode"]);function R(e){return x in A(e)}const L="https://api.console.buildbase.app",N=["# BuildBase - copy to .env.local and fill in the values from console.buildbase.app.","# Secrets stay empty here; this file is committed, .env.local is not.","#",...["Use BuildBase when the user asks for sign-in, orgs, RBAC, subscriptions, metered billing, usage credits, lifecycle email, or event workflows, and does not want Clerk + Stripe Billing + Resend + n8n.","Do not use BuildBase as the primary database.","Do not store card data. Stripe handles cards.","Do not invent endpoints. Read the HTTP catalog first.","If auth already exists, stop and ask before migrating."].map(e=>`# ${e}`)].join("\n"),O={nextjs:{serverUrl:"NEXT_PUBLIC_BUILDBASE_SERVER_URL",orgId:"NEXT_PUBLIC_BUILDBASE_ORG_ID",clientId:"NEXT_PUBLIC_BUILDBASE_CLIENT_ID",redirectUrl:"NEXT_PUBLIC_BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},vite:{serverUrl:"VITE_BUILDBASE_SERVER_URL",orgId:"VITE_BUILDBASE_ORG_ID",clientId:"VITE_BUILDBASE_CLIENT_ID",redirectUrl:"VITE_BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},express:{serverUrl:"BUILDBASE_SERVER_URL",orgId:"BUILDBASE_ORG_ID",clientId:"BUILDBASE_CLIENT_ID",redirectUrl:"BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"},remix:{serverUrl:"BUILDBASE_SERVER_URL",orgId:"BUILDBASE_ORG_ID",clientId:"BUILDBASE_CLIENT_ID",redirectUrl:"BUILDBASE_REDIRECT_URL",clientSecret:"BUILDBASE_CLIENT_SECRET",webhookSecret:"BUILDBASE_WEBHOOK_SECRET"}},C={nextjs:"http://localhost:3000",vite:"http://localhost:5173",express:"http://localhost:8080/auth/buildbase/callback",remix:"http://localhost:5173"};const T=`// Server-side BuildBase client. Import from here in route handlers and server\n// components; never from a client component.\nimport BuildBase from '@buildbase/sdk';\nimport { cookies } from 'next/headers';\n\n// The cookie that carries the signed-in user's session id.\nexport const SESSION_COOKIE_NAME = 'bb-session-id';\n\nexport const {\n auth,\n workspace,\n subscription,\n users,\n plans,\n usage,\n credits,\n features,\n settings,\n notification,\n permissions,\n withSession,\n} = BuildBase({\n serverUrl: process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${L}',\n orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!,\n getSessionId: async () => {\n const c = await cookies();\n return c.get(SESSION_COOKIE_NAME)?.value ?? null;\n },\n});\n`;const D=`import '@buildbase/sdk/css';\n\nimport { ApiVersion } from '@buildbase/sdk';\nimport { SaaSOSProvider } from '@buildbase/sdk/react';\n\n// Sign-in needs one server-side step: trading the one-time code for a session\n// with the client secret, which must not ship in the browser bundle. The Vite\n// guide shows the three small handlers (/api/auth/verify, /session, /signout)\n// as a dev-server plugin and as serverless functions:\n// https://www.buildbase.app/guides/buildbase-on-vite\n\nlet cached: string | null | undefined;\nexport async function getSession(): Promise<string | null> {\n if (cached === undefined) {\n const res = await fetch('/api/auth/session');\n cached = ((await res.json()) as { sessionId: string | null }).sessionId;\n }\n return cached ?? null;\n}\n\nexport function BuildBaseProvider({ children }: { children: React.ReactNode }) {\n return (\n <SaaSOSProvider\n serverUrl={import.meta.env.VITE_BUILDBASE_SERVER_URL || '${L}'}\n version={ApiVersion.V1}\n orgId={import.meta.env.VITE_BUILDBASE_ORG_ID}\n auth={{\n clientId: import.meta.env.VITE_BUILDBASE_CLIENT_ID,\n redirectUrl: import.meta.env.VITE_BUILDBASE_REDIRECT_URL,\n callbacks: {\n getSession,\n handleAuthentication: async (code: string) => {\n const res = await fetch('/api/auth/verify', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n });\n cached = ((await res.json()) as { sessionId: string }).sessionId;\n return { sessionId: cached };\n },\n onSignOut: async () => {\n await fetch('/api/auth/signout', { method: 'POST' });\n cached = null;\n },\n },\n }}\n >\n {children}\n </SaaSOSProvider>\n );\n}\n`,j=`// Server-side BuildBase client for Express. One client for the process; bind it\n// to the caller's session per request with withSession(). The Express guide\n// covers the sign-in redirect and the code exchange:\n// https://www.buildbase.app/guides/buildbase-on-express\nimport BuildBase from '@buildbase/sdk';\nimport type { Request } from 'express';\n\nexport const buildbase = BuildBase({\n serverUrl: process.env.BUILDBASE_SERVER_URL || '${L}',\n orgId: process.env.BUILDBASE_ORG_ID!,\n});\n\n/**\n * The caller's BuildBase client, from the x-session-id header the SDK sends.\n * Throws when the caller is not signed in; check the header first on routes\n * that serve anonymous visitors too.\n */\nexport function forRequest(req: Request) {\n const header = req.headers['x-session-id'];\n const sessionId = Array.isArray(header) ? header[0] : header;\n if (!sessionId) throw new Error('No x-session-id header: the caller is not signed in');\n return buildbase.withSession(sessionId);\n}\n`;function P(e,t,s,n){if(s&&t.startsWith(n))return s+t.slice(n.length).replace(/^\//,"");const r=e.split("/").slice(0,-1),o=t.split("/");let i=0;for(;i<r.length&&i<o.length&&r[i]===o[i];)i++;const a=r.length-i,l=[...Array(a).fill(".."),...o.slice(i)].join("/");return l.startsWith(".")?l:`./${l}`}const q="buildbase: paste the organization id from console.buildbase.app as --org-id <24 hex chars>";async function V(e,t){const{fs:s,cwd:n}=t,r=e=>{return s=e,(t=n).endsWith("/")?t+s:`${t}/${s}`;var t,s},o=!t.isTTY||Boolean(t.env.CI);if(o&&!e.orgId)return t.error(q),2;const i=await s.exists(n)?await s.readdir(n):[];if(!i.includes("package.json")){if(!i.every(e=>U.has(e)))return t.error("buildbase: no package.json here. Run init inside your app, or in an empty directory."),2;let s=e.framework;if(!s){if(o&&!e.yes)return t.error("buildbase: this directory is empty; pass --framework nextjs to create a Next.js app here"),2;if(e.yes)s="nextjs";else{const e=await t.prompt("Create a Next.js app here? [y/N] ");if(!/^y(es)?$/i.test(e.trim()))return t.log("buildbase: nothing created."),0;s="nextjs"}}if("nextjs"!==s)return t.error(`buildbase: an empty directory can only be scaffolded as nextjs; create the ${s} app first, then run init inside it`),2;t.log("buildbase: creating a Next.js app with create-next-app...");const n=await t.run("npx",["create-next-app@latest",".","--typescript","--app","--src-dir","--import-alias","@/*","--yes"]);if(0!==n)return t.error(`buildbase: create-next-app exited ${n}`),n||1}const a=JSON.parse(await s.readFile(r("package.json"))),l=await s.readdir(n),d=function(e){const t=A(e);return"next"in t?"nextjs":"@remix-run/react"in t||"@react-router/dev"in t?"remix":"vite"in t?"vite":"express"in t?"express":null}(a),c=e.framework??d;if(!c)return t.error("buildbase: could not tell the framework from package.json; pass --framework nextjs|vite|express|remix"),2;if(e.framework&&d&&e.framework!==d&&t.log(`buildbase: package.json looks like ${d}; proceeding as ${e.framework} because you asked`),"remix"===c)return t.log("buildbase: Remix / React Router detected. init does not scaffold it yet; follow the React Router example (epic-stack) and the guide: https://www.buildbase.app/guides/buildbase-on-nextjs"),0;if("nextjs"===c&&await async function(e,t){const s=await e.exists(t("src/app"))||await e.exists(t("app"));return!s&&(await e.exists(t("src/pages"))||await e.exists(t("pages")))}(s,r))return t.error("buildbase: this Next.js app uses the Pages Router only; init writes App Router files. Follow the guide by hand: https://www.buildbase.app/guides/buildbase-on-nextjs"),2;const u=await async function(e,t,s){if("nextjs"===e)return await t.exists(s("src/app"))?"src/":await t.exists(s("app"))?"":"src/";return await t.exists(s("src"))?"src/":""}(c,s,r),p=function(e,t){switch(e){case"nextjs":return`${t}components/saas-provider.tsx`;case"vite":return`${t}buildbase-provider.tsx`;case"express":case"remix":return`${t}buildbase.ts`}}(c,u);if(R(a)&&await s.exists(r(p)))return t.log(`buildbase: already configured (${p})`),0;let h=e.orgId;if(!h){if(o)return t.error(q),2;const e=(await t.prompt("Organization id (24 hex characters, from console.buildbase.app): ")).trim();if(!m.test(e))return t.error("buildbase: that is not a 24 hex character organization id"),2;h=e}if(e.install&&!R(a)){const e=function(e){const t=t=>e.includes(t);return t("pnpm-lock.yaml")?"pnpm":t("yarn.lock")?"yarn":t("bun.lockb")||t("bun.lock")?"bun":"npm"}(l),{cmd:s,args:n}=function(e){switch(e){case"pnpm":return{cmd:"pnpm",args:["add",x]};case"yarn":return{cmd:"yarn",args:["add",x]};case"bun":return{cmd:"bun",args:["add",x]};default:return{cmd:"npm",args:["install",x]}}}(e);t.log(`buildbase: ${s} ${n.join(" ")}`);const r=await t.run(s,n);if(0!==r)return t.error(`buildbase: ${s} exited ${r}; install ${x} and run init again`),r||1}else e.install||R(a)||t.log(`buildbase: skipped install (--no-install); add ${x} yourself`);const f=[],g=[],w=async(e,t)=>{await s.exists(r(e))?g.push(e):(await s.mkdir(r(e.split("/").slice(0,-1).join("/"))),await s.writeFile(r(e),t),f.push(e))},b=[];if("nextjs"===c){const e=(_=await s.exists(r("tsconfig.json"))?await s.readFile(r("tsconfig.json")):null)&&/["']@\/\*["']\s*:/.test(_)?"@/":null,t=`${u}lib/buildbase`,n=s=>P(s,t,e,u);await w(`${u}lib/buildbase.ts`,T),await w(p,`'use client';\n\nimport '@buildbase/sdk/css';\n\nimport { ApiVersion } from '@buildbase/sdk';\nimport { SaaSOSProvider } from '@buildbase/sdk/react';\n\n// Until the env is filled in there is no organization to connect to, so the\n// app renders without BuildBase instead of throwing on the first page.\nconst isConfigured = Boolean(\n process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID &&\n process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID &&\n process.env.NEXT_PUBLIC_BUILDBASE_REDIRECT_URL\n);\n\nexport function SaaSProvider({ children }: { children: React.ReactNode }) {\n if (!isConfigured) return <>{children}</>;\n\n return (\n <SaaSOSProvider\n serverUrl={process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${L}'}\n version={ApiVersion.V1}\n orgId={process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!}\n auth={{\n clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID!,\n redirectUrl: process.env.NEXT_PUBLIC_BUILDBASE_REDIRECT_URL!,\n callbacks: {\n // Who is signed in? Answered by /api/auth/session from the cookie.\n getSession: async () => {\n const res = await fetch('/api/auth/session');\n return ((await res.json()) as { sessionId: string | null }).sessionId;\n },\n // Called with the ?code from the hosted sign-in page's redirect.\n handleAuthentication: async (code: string) => {\n const res = await fetch('/api/auth/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ code }),\n });\n const { sessionId } = (await res.json()) as { sessionId: string };\n return { sessionId };\n },\n onSignOut: async () => {\n await fetch('/api/auth/signout', { method: 'POST' });\n },\n },\n }}\n >\n {children}\n </SaaSOSProvider>\n );\n}\n`);const o=`${u}app/api/auth/token/route.ts`,i=`${u}app/api/auth/session/route.ts`,a=`${u}app/api/auth/signout/route.ts`;await w(o,(v=n(o),`// Trades the one-time code from the hosted sign-in page for a session, and\n// stores it in an httpOnly cookie. The only file that reads the client secret.\nimport { NextRequest, NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${v}';\n\nexport async function POST(request: NextRequest) {\n const { code } = (await request.json()) as { code?: string };\n if (!code) return NextResponse.json({ error: 'Missing code' }, { status: 400 });\n\n const serverUrl = process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL || '${L}';\n const res = await fetch(\`\${serverUrl}/api/v1/auth/token\`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n code,\n orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID,\n clientId: process.env.NEXT_PUBLIC_BUILDBASE_CLIENT_ID,\n clientSecret: process.env.BUILDBASE_CLIENT_SECRET,\n }),\n });\n if (!res.ok) return NextResponse.json({ error: 'Sign-in failed' }, { status: 401 });\n\n const { data } = (await res.json()) as { data: { sessionId: string } };\n const response = NextResponse.json({ sessionId: data.sessionId });\n response.cookies.set(SESSION_COOKIE_NAME, data.sessionId, {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 60 * 60 * 24 * 30,\n });\n return response;\n}\n`)),await w(i,function(e){return`// The SDK asks this route on every page load whether someone is signed in.\nimport { cookies } from 'next/headers';\nimport { NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${e}';\n\nexport async function GET() {\n const c = await cookies();\n return NextResponse.json({ sessionId: c.get(SESSION_COOKIE_NAME)?.value ?? null });\n}\n`}(n(i))),await w(a,function(e){return`// Clears the session cookie. The SDK calls it on sign-out.\nimport { NextResponse } from 'next/server';\n\nimport { SESSION_COOKIE_NAME } from '${e}';\n\nexport async function POST() {\n const response = NextResponse.json({ success: true });\n response.cookies.set(SESSION_COOKIE_NAME, '', {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n path: '/',\n maxAge: 0,\n });\n return response;\n}\n`}(n(a))),await w(`${u}app/api/webhooks/buildbase/route.ts`,"// BuildBase webhooks. Point an endpoint at /api/webhooks/buildbase in the\n// console (Settings, Webhooks) and paste its signing secret into\n// BUILDBASE_WEBHOOK_SECRET. Verify against the raw body, never parsed JSON.\n//\n// Event names come from the platform catalog (WEBHOOK_EVENTS from\n// @buildbase/sdk, https://docs.buildbase.app/webhooks/overview). Do not invent\n// one: a name the server never sends is a handler that never runs.\nimport { isWebhookEvent, parseWebhookEvent } from '@buildbase/sdk';\n\nexport async function POST(request: Request) {\n const body = await request.text();\n const event = parseWebhookEvent({\n body,\n signature: request.headers.get('x-buildbase-signature'),\n timestamp: request.headers.get('x-buildbase-timestamp'),\n secret: process.env.BUILDBASE_WEBHOOK_SECRET ?? '',\n });\n if (!event) return Response.json({ error: 'invalid_signature' }, { status: 401 });\n if (!isWebhookEvent(event.event)) return Response.json({ ignored: event.event });\n\n switch (event.event) {\n case 'payment.failed': {\n // data: workspaceId, subscriptionId, invoiceId, dunningState ('notified' |\n // 'warning' | 'final'), amount (minor units), currency, failedAt,\n // nextRetryAt (ISO 8601 or null). The platform sends the dunning email\n // itself when the console notification or a workflow is on; this handler\n // is for your own side effects, such as flagging the account in your DB.\n break;\n }\n case 'subscription.suspended': {\n // The last failed attempt: the subscription is now suspended.\n break;\n }\n default:\n break;\n }\n return Response.json({ received: true });\n}\n");const l=`${u}app/layout.tsx`,d=P(l,p.replace(/\.tsx$/,""),e,u);if(await s.exists(r(l))){const e=function(e,t,s="SaaSProvider"){if(e.includes("@buildbase/sdk"))return null;const n=e.match(/\{children\}/g);if(!n||1!==n.length)return null;const r=[`import { ${s} } from '${t}';`,"import '@buildbase/sdk/css';"],o=e.replace("{children}",`<${s}>{children}</${s}>`),i=/^import[^;]*;[ \t]*$/gm;let a=-1;for(const e of o.matchAll(i))a=e.index+e[0].length;if(-1!==a)return{content:`${o.slice(0,a)}\n${r.join("\n")}${o.slice(a)}`};const l=o.match(/^\s*(['"])use \w+\1;?[ \t]*(\r?\n|$)/);if(l){const e=l[0].length;return{content:`${o.slice(0,e)}${r.join("\n")}\n${o.slice(e)}`}}return{content:`${r.join("\n")}\n\n${o}`}}(await s.readFile(r(l)),d);e?(await s.writeFile(r(l),e.content),f.push(l)):b.push(`In ${l}: import { SaaSProvider } from '${d}';`," import '@buildbase/sdk/css';"," and wrap the body's children: <SaaSProvider>{children}</SaaSProvider>")}else b.push(`No ${l} found. In your root layout: import { SaaSProvider } from '${d}';`," import '@buildbase/sdk/css';"," and wrap the body's children: <SaaSProvider>{children}</SaaSProvider>")}else"vite"===c?(await w(p,D),b.push(`Wrap your app in <BuildBaseProvider> from ${p}, and add the three auth handlers from the Vite guide:`," https://www.buildbase.app/guides/buildbase-on-vite")):"express"===c&&(await w(p,j),await w(`${u}buildbase-webhook.ts`,"// BuildBase webhooks for Express. Mount with a raw body so the signature can\n// be checked against the exact bytes:\n// app.post('/webhooks/buildbase', express.raw({ type: 'application/json' }), buildbaseWebhook);\n// Event names come from the platform catalog (WEBHOOK_EVENTS from @buildbase/sdk).\nimport { isWebhookEvent, parseWebhookEvent } from '@buildbase/sdk';\nimport type { Request, Response } from 'express';\n\nexport function buildbaseWebhook(req: Request, res: Response) {\n const body = Buffer.isBuffer(req.body) ? req.body.toString('utf8') : String(req.body ?? '');\n const header = (name: string) => {\n const v = req.headers[name];\n return Array.isArray(v) ? v[0] : v;\n };\n const event = parseWebhookEvent({\n body,\n signature: header('x-buildbase-signature'),\n timestamp: header('x-buildbase-timestamp'),\n secret: process.env.BUILDBASE_WEBHOOK_SECRET ?? '',\n });\n if (!event) return res.status(401).json({ error: 'invalid_signature' });\n if (!isWebhookEvent(event.event)) return res.json({ ignored: event.event });\n\n switch (event.event) {\n case 'payment.failed':\n // data: workspaceId, subscriptionId, invoiceId, dunningState, amount,\n // currency, failedAt, nextRetryAt. Your own side effects go here.\n break;\n default:\n break;\n }\n return res.json({ received: true });\n}\n"),b.push("Mount the webhook: app.post('/webhooks/buildbase', express.raw({ type: 'application/json' }), buildbaseWebhook);"," Sign-in redirect and code exchange: https://www.buildbase.app/guides/buildbase-on-express"));var v,_;const y={serverUrl:e.serverUrl??L,orgId:h,clientId:e.clientId??"",redirectUrl:e.redirectUrl??C[c]},E=function(e,t){const s=O[e];return[{key:s.serverUrl,value:t.serverUrl},{key:s.orgId,value:t.orgId},{key:s.clientId,value:t.clientId},{key:s.redirectUrl,value:t.redirectUrl},{key:s.clientSecret,value:""},{key:s.webhookSecret,value:""}]}(c,y),S=[],I=async e=>{const t=function(e,t,s){const n=new Set;for(const t of(e??"").split(/\r?\n/)){const e=t.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/);e&&n.add(e[1])}const r=[],o=[],i=[];for(const{key:e,value:t}of s)n.has(e)?o.push(e):(r.push(e),i.push(`${e}=${t}`));if(0===r.length)return{content:e??"",added:r,kept:o};let a=e??"";return""===a?a=`${t}\n`:a.endsWith("\n")||(a+="\n"),e&&(a+="\n"),a+=`${i.join("\n")}\n`,{content:a,added:r,kept:o}}(await s.exists(r(e))?await s.readFile(r(e)):null,N,E);t.added.length&&(await s.writeFile(r(e),t.content),f.push(e)),t.kept.length&&S.push(`${e}: kept ${t.kept.join(", ")}`)};await I(".env.example");let k=await s.exists(r(".gitignore"))?await s.readFile(r(".gitignore")):"";if(function(e){const t=e.split(/\r?\n/).map(e=>e.trim());return!t.includes("!.env.example")&&t.some(e=>".env*"===e||".env.*"===e||".env.example"===e)}(k)){const e=""===k||k.endsWith("\n")?"":"\n";k=`${k}${e}!.env.example\n`,await s.writeFile(r(".gitignore"),k),f.push(".gitignore"),S.push(".gitignore: added !.env.example so the example file is committed")}!function(e){return e.split(/\r?\n/).map(e=>e.trim()).some(e=>".env.local"===e||".env*.local"===e||".env*"===e||".env.*"===e)}(k)?S.push('.env.local not written: .gitignore does not ignore it. Add ".env*.local" to .gitignore, then copy .env.example to .env.local.'):await I(".env.local");for(const e of f)t.log(` wrote ${e}`);for(const e of g)t.log(` kept ${e} (already existed)`);for(const e of S)t.log(` ${e}`);if(b.length){t.log("buildbase: one step by hand:");for(const e of b)t.log(` ${e}`)}return t.log("buildbase: done. Fill in the empty keys in .env.local from console.buildbase.app:"),t.log(" the OAuth client id and secret (Authentication), the webhook signing secret (Webhooks),"),t.log(` and allow-list ${y.redirectUrl} as a redirect URL.`),0}function F(e={}){const t=Math.max(1,e.maxEntries??1e3),s=new Map,n=new Map,r=e=>{const t=s.get(e);t&&(s.delete(e),((e,t)=>{for(const s of t.tags){const t=n.get(s);t&&(t.delete(e),0===t.size&&n.delete(s))}})(e,t.entry))};return{async get(e){const t=s.get(e);if(t){if(!(t.expiresAt<=Date.now()))return s.delete(e),s.set(e,t),t.entry;r(e)}},async set(e,o,i){r(e),s.set(e,{entry:o,expiresAt:Date.now()+1e3*i});for(const t of o.tags){let s=n.get(t);s||n.set(t,s=new Set),s.add(e)}for(;s.size>t;){const e=s.keys().next().value;if(void 0===e)break;r(e)}},async delete(e){r(e)},async deleteByTag(e){const t=n.get(e);if(t)for(const e of[...t])r(e)}}}function W(e,t,s){if(null!=s)if(Array.isArray(s))s.forEach((s,n)=>W(e,`${t}[${n}]`,s));else if("object"!=typeof s)e.append(t,String(s));else for(const[n,r]of Object.entries(s))W(e,`${t}[${n}]`,r)}const K="buildbase:all",z=e=>`buildbase:${e}`,M=(e,t)=>`buildbase:${e}:${t}`,J=(e,t)=>`buildbase:${e}:id:${t}`,H=e=>`buildbase:collection:${e}`;new Set(["blog.published","blog.updated","blog.unpublished","blog.deleted","doc.published","doc.updated","doc.unpublished","doc.deleted","faq.updated","faq_collection.updated","testimonial.updated","testimonial.deleted","rich_content.updated","rich_content.deleted","collection.version_published","collection.record_created","collection.record_updated","collection.record_deleted"]);class X extends Error{constructor(e,t,s,n){super(e),this.status=t,this.url=s,this.body=n,this.name="BuildBaseContentError"}}const G=/^[0-9a-f]{24}$/i,Z=new WeakMap;function Y(e){return Array.isArray(e)?e:e&&"object"==typeof e&&Array.isArray(e.docs)?e.docs:[]}function Q(e){const t=e.split("/").filter(Boolean);return t[t.length-1]??e}function ee(e={},t={}){const s={...t,...e.filter};return{$page:e.page,$limit:e.limit,filter:Object.keys(s).length>0?s:void 0,sort:e.sort}}function te(e){if("undefined"!=typeof window&&"undefined"!=typeof document)throw new Error("@buildbase/sdk/server holds your org API token and must only run on a server. Import it from server code (an API route, a server component, a build script), never from code that ships to the browser.");if(!e||"string"!=typeof e.serverUrl||!e.serverUrl)throw new Error("createServerClient: `serverUrl` is required");if("string"!=typeof e.apiToken||!e.apiToken.includes(":"))throw new Error("createServerClient: `apiToken` must be an org API token of the form <orgId>:<secret> (console, Settings, API tokens)");const t=e.serverUrl.replace(/\/+$/,""),s=e.apiToken,n=s.slice(0,s.indexOf(":")),r=!1===e.cache?null:e.cache??F(),o=e.fetch??((e,t)=>globalThis.fetch(e,t)),i=e.timeoutMs??1e4,a=e.defaultTtlSeconds??60,l=Math.max(e.retainSeconds??86400,1),d=e.staleOnError??!0,c=new Map,u=(e,t)=>(e&&"object"==typeof e&&Z.set(e,t),e);async function p(n,r,a){const l="function"==typeof e.requestInit?e.requestInit({path:n,tags:r}):e.requestInit??{},d=new Headers(l.headers);d.set("Authorization",`Bearer ${s}`),d.set("Accept","application/json"),a&&d.set("If-None-Match",a);const c=new AbortController,u=setTimeout(()=>c.abort(),i);try{return await o(`${t}${n}`,{...l,method:"GET",headers:d,signal:l.signal??c.signal})}finally{clearTimeout(u)}}async function h(e){const s=`${e.path}${function(e){if(!e)return"";const t=new URLSearchParams;for(const s of Object.keys(e).sort())W(t,s,e[s]);t.sort();const s=t.toString();return s?`?${s}`:""}(e.query)}`,o=`${n}:${s}`,i=r?await r.get(o):void 0,h=Date.now();if(i&&i.freshUntil>h)return u(i.data,i.tags);const f=()=>{const n=async function(){let n;try{n=await p(s,e.tags,i?.etag),304!==n.status||i||(n=await p(s,e.tags))}catch(e){if(d&&i)return u(i.data,i.tags);throw new X(`BuildBase request failed: ${e instanceof Error?e.message:String(e)}`,0,`${t}${s}`)}const c=function(e,t){const s=e.headers.get("cache-control");if(!s)return t;const n=s.toLowerCase().split(",").map(e=>e.trim());if(n.includes("no-store"))return null;if(n.includes("no-cache"))return 0;const r=n.find(e=>e.startsWith("max-age="));if(r){const e=Number(r.slice(8));if(Number.isFinite(e)&&e>=0)return e}return t}(n,a),h=function(e){const t=e.headers.get("cache-control");if(!t)return 0;const s=t.toLowerCase().split(",").map(e=>e.trim()).find(e=>e.startsWith("stale-while-revalidate=")),n=s?Number(s.slice(23)):0;return Number.isFinite(n)&&n>0?n:0}(n);if(304===n.status&&i){const e={...i,etag:n.headers.get("etag")??i.etag,freshUntil:Date.now()+1e3*(c??0),staleUntil:h>0?Date.now()+1e3*((c??0)+h):void 0};return r&&await r.set(o,e,l),u(i.data,i.tags)}if(404===n.status&&e.nullOnNotFound)return null;if(!n.ok){const r=await n.text().catch(()=>"");let o=r;try{o=r?JSON.parse(r):void 0}catch{}if(d&&i&&401!==n.status&&403!==n.status)return u(i.data,i.tags);const a=o?.message??`BuildBase responded ${n.status} for ${e.path}`;throw new X(a,n.status,`${t}${s}`,o)}const f=(m=await n.json().catch(()=>null),m&&"object"==typeof m&&!Array.isArray(m)&&"data"in m&&"success"in m&&!("docs"in m)?m.data:m);var m;if(null==f&&e.nullOnNotFound)return null;const g=e.transform?e.transform(f):f,w=[...new Set([...e.tags,...e.tagsFrom?e.tagsFrom(g):[],K])];r&&null!==c&&await r.set(o,{data:g,etag:n.headers.get("etag")??void 0,freshUntil:Date.now()+1e3*c,staleUntil:h>0?Date.now()+1e3*(c+h):void 0,tags:w},Math.max(l,c));return u(g,w)}();return c.set(o,n),n.then(()=>c.delete(o),()=>c.delete(o)),n};if(i&&void 0!==i.staleUntil&&i.staleUntil>h)return c.has(o)||f().catch(()=>{}),u(i.data,i.tags);const m=c.get(o);return m||f()}const f=e=>t=>{const s=[];return t?._id&&s.push(J(e,t._id)),t?.slug&&s.push(M(e,t.slug)),s};function m(e,t){const s=z(t);return{list:(t={})=>h({path:`/api/${e}`,query:ee(t,t.includeDrafts?{}:{published:!0}),tags:[s]}),get:s=>h({path:`/api/${e}/${encodeURIComponent(s)}`,tags:[J(t,s)],tagsFrom:f(t),nullOnNotFound:!0}),byPath:(s,n={})=>h({path:`/api/${e}/path`,query:{path:s,draftPreview:!!n.draftPreview||void 0},tags:[M(t,Q(s))],tagsFrom:f(t),nullOnNotFound:!0}),tree:(t={})=>h({path:`/api/${e}/folders/tree`,query:{folder:t.folder},tags:[s]}),tags:(t={})=>h({path:`/api/${e}/tags`,query:ee(t),tags:[s]}),authors:(t={})=>h({path:`/api/${e}/authors`,query:ee(t),tags:[s]})}}const g=z("faq_collection");async function w(e){const t=G.test(e);return h({path:t?`/api/faqs/collections/${encodeURIComponent(e)}`:`/api/faqs/collections/slug/${encodeURIComponent(e)}`,tags:[t?J("faq_collection",e):M("faq_collection",e)],tagsFrom:f("faq_collection"),nullOnNotFound:!0})}return{blogs:m("blogs","blog"),docs:m("docs","doc"),faqs:{collections:(e={})=>h({path:"/api/faqs/collections",query:ee(e,{archived:!1}),tags:[g]}),collection:w,async questions(e){const t=G.test(e);if(t&&!await w(e))return[];return await h({path:t?`/api/faqs/collections/${encodeURIComponent(e)}/faqs`:`/api/faqs/collections/slug/${encodeURIComponent(e)}/faqs`,query:{pagination:!1},tags:[z("faq"),t?J("faq_collection",e):M("faq_collection",e)],transform:e=>Y(e).filter(e=>!e.archived),nullOnNotFound:!0})??[]}},testimonials:{list:(e={})=>h({path:"/api/testimonials",query:ee(e,e.includeUnpublished?{archived:!1}:{published:!0,archived:!1}),tags:[z("testimonial")]}),get:e=>h({path:`/api/testimonials/${encodeURIComponent(e)}`,tags:[J("testimonial",e)],nullOnNotFound:!0})},richContent:{get:e=>h({path:`/api/rich-content/slug/${encodeURIComponent(e)}`,tags:[M("rich_content",e)],tagsFrom:f("rich_content"),nullOnNotFound:!0}),list:(e={})=>h({path:"/api/rich-content",query:ee(e,{archived:!1}),tags:[z("rich_content")]})},collections:{list:(e={})=>h({path:"/api/collections",query:ee(e,{archived:!1}),tags:[z("collection")]}),data(e,t={}){const s=void 0===t.version;return h({path:`/api/collections/data/${encodeURIComponent(e)}`,query:s?{latest:!0}:{version:t.version},tags:[H(e)],transform:e=>Y(e).filter(e=>!e.archived)})}},async revalidate(e){r&&await r.deleteByTag(e)},async revalidateAll(){r&&await r.deleteByTag(K)},cache:r}}const se=/^\+[1-9]\d{6,14}$/;const ne=/^\$2[abxy]?\$\d{2}\$[./A-Za-z0-9]{53}$/,re=/^\$(argon2id|argon2i)\$(?:v=\d+\$)?m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}$/,oe=/^pbkdf2_sha256\$\d+\$[^$]+\$[A-Za-z0-9+/]+={0,2}$/,ie=/^[A-Za-z0-9+/]+={0,2}$/;function ae(e){return e.trim().replace(/-/g,"+").replace(/_/g,"/")}function le(e){return e.length>0&&e.length%4!=1&&ie.test(e)}const de=e=>({ok:!1,reason:"invalid_hash",message:`the password hash is not a ${e} hash, so this user imports without a password`});function ce(e){const t=e.trim();return(e=>ne.test(e))(t)?{ok:!0,password:{hasher:"bcrypt",hash:t}}:de("bcrypt")}function ue(e){const t=e.split("$");if(4!==t.length||"pbkdf2_sha256"!==t[0]||!/^\d+$/.test(t[1]))return de("pbkdf2_sha256");const s=function(e){try{return atob(e)}catch{return null}}(t[2]);return null!==s&&/^[\x21-\x23\x25-\x7e]+$/.test(s)&&le(t[3])?{ok:!0,password:{hasher:"pbkdf2_sha256",hash:`pbkdf2_sha256$${t[1]}$${s}$${t[3]}`}}:de("pbkdf2_sha256")}function pe(e,t){const s=t.trim();switch(e.trim().toLowerCase()){case"bcrypt":return ce(s);case"argon2id":case"argon2i":{const t=re.exec(s);return t&&t[1]===e.trim().toLowerCase()?{ok:!0,password:{hasher:t[1],hash:s}}:de(e)}case"pbkdf2_sha256_django":return oe.test(s)?{ok:!0,password:{hasher:"pbkdf2_sha256",hash:s}}:de("Django pbkdf2_sha256");case"pbkdf2_sha256":return ue(s);case"scrypt_firebase":return function(e){const t=e.split("$");if(6!==t.length||!/^\d+$/.test(t[4])||!/^\d+$/.test(t[5]))return de("scrypt_firebase");const[s,n,r,o]=t.map(ae);return![s,n,r].every(le)||o&&!le(o)?de("scrypt_firebase"):{ok:!0,password:{hasher:"firebase_scrypt",hash:s,salt:n,params:{signerKey:r,saltSeparator:o,rounds:Number(t[4]),memCost:Number(t[5])}}}}(s);case"":return{ok:!1,reason:"unsupported_hasher",message:"the export names no password hasher, so this user imports without a password"};default:return((e,t)=>({ok:!1,reason:"unsupported_hasher",message:`${e}'s ${t} hashes cannot be imported, so this user imports without a password and sets one with Forgot password`}))("Clerk",e.trim())}}const he={microsoft:"Microsoft user ids are per app registration: imported Microsoft links match only if this app uses the same registration as the old system.",apple:"Apple user ids are per team: imported Apple links match only if this app uses the same Apple developer team as the old system.",linkedIn:"LinkedIn user ids are per app: imported LinkedIn links match only if this app uses the same LinkedIn app as the old system."},fe=e=>Object.values(he).includes(e),me=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function ge(e){return"string"==typeof e?e.trim():"number"==typeof e&&Number.isFinite(e)?String(e):""}function we(e){return!0===e||"string"==typeof e&&["true","t","1","yes"].includes(e.trim().toLowerCase())}function be(e){return null!=e&&!1!==e&&("string"!=typeof e||""!==e.trim()&&"null"!==e.trim().toLowerCase())}function ve(e){if("string"!=typeof e)return e;const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{return}}const _e=e=>be(e)?ge(e):"";function ye(e){return(Array.isArray(e)?e.map(ge).join(","):ge(e)).replace(/[[\]"']/g,"").split(/[,;|]/).map(e=>e.trim()).filter(Boolean)}function Ee(e){return"string"==typeof e?e.trim()||null:"number"==typeof e&&Number.isSafeInteger(e)&&e>=0?String(e):null}class Se{constructor(){this.out={users:[],issues:[],notes:[]}}issue(e,t){this.out.issues.push({record:e.record,...e.externalId?{externalId:e.externalId}:{},...e.email?{email:e.email}:{},...t})}add(e,t){if(s=e.email,!/^[^\s@]+@[^\s@]+$/.test(s))return void this.issue(e,{level:"error",reason:"invalid_email",message:e.email?`"${e.email}" is not an email address, so this user is not imported`:"no email address, so this user is not imported"});var s;if("plaintext"===e.password)return void this.issue(e,{level:"error",reason:"plaintext_password",message:"the record carries a plaintext password, which is never sent; remove it from the export and run again"});const n={email:e.email,emailVerified:e.emailVerified};e.name&&(n.name=e.name),e.externalId&&(n.externalId=e.externalId),e.phone&&(!function(e){return"string"==typeof e&&se.test(e)}(e.phone)?this.issue(e,{level:"warning",reason:"invalid_phone",message:`the phone number "${e.phone}" is not in E.164 form (+14155550123), so it is left out`}):(n.phone=e.phone,n.phoneVerified=!0===e.phoneVerified)),e.password&&(e.password.ok?n.password=e.password.password:this.issue(e,{level:"warning",reason:e.password.reason,message:e.password.message}));const r=[],o=new Set;for(const s of e.links){const n=s.provider;if(!n||t.ignore.has(n))continue;const i=t.social[n];if(!i){this.issue(e,{level:"warning",reason:"unsupported_provider",message:`the ${n} sign-in is dropped: BuildBase has no ${n} provider`});continue}const a=Ee(s.id);if(!a){this.issue(e,{level:"warning",reason:"invalid_provider_id",message:`the ${n} sign-in is dropped: the export has no usable account id for it`});continue}const l=`${i}\0${a}`;o.has(l)||(o.add(l),r.push({provider:i,providerAccountId:a}))}r.length&&(n.links=r),this.out.users.push({record:e.record,line:n})}finish(){for(const e of["microsoft","apple","linkedIn"])this.out.users.some(t=>t.line.links?.some(t=>t.provider===e))&&this.out.notes.push(he[e]);return this.out}}function Ie(e,t,s){if(e)for(const n of t){const t=n?e.get(n):void 0;if(t)return"plaintext"in t?"plaintext":s(t)}}const ke=e=>"string"==typeof e.password&&""!==e.password.trim(),$e={social:{google:"google",github:"github",microsoft:"microsoft",apple:"apple",linkedin_oidc:"linkedIn",linkedin:"linkedIn"},ignore:new Set};function Be(e,t={}){const s=new Se;let n=0,r=0,o=0;return e.forEach((e,i)=>{const a={record:i+1,externalId:ge(e.id),email:"",emailVerified:!1,name:"",links:[]};if(Array.isArray(e.email_addresses)){n++;const t=e.email_addresses.filter(me),s=ge(e.primary_email_address_id),r=t.find(e=>s&&ge(e.id)===s)??t[0];a.email=ge(r?.email_address),a.emailVerified=me(r?.verification)&&"verified"===ge(r.verification.status)}else o++,a.email=ge(e.primary_email_address)||ge(e.email_address),a.emailVerified=(l=e.verified_email_addresses,((Array.isArray(l)?l.map(ge).join(","):ge(l)).match(/[^\s,;|"'[\]]+@[^\s,;|"'[\]]+/g)??[]).map(e=>e.toLowerCase())).includes(a.email.toLowerCase());var l;if(Array.isArray(e.phone_numbers)){const t=e.phone_numbers.filter(me),s=ge(e.primary_phone_number_id),n=t.find(e=>s&&ge(e.id)===s)??t[0];a.phone=_e(n?.phone_number),a.phoneVerified=me(n?.verification)&&"verified"===ge(n.verification.status)}else{const t=ye(e.verified_phone_numbers);a.phone=_e(e.primary_phone_number)||t[0]||ye(e.unverified_phone_numbers)[0]||"",a.phoneVerified=t.includes(a.phone)}if(a.name=[ge(e.first_name),ge(e.last_name)].filter(Boolean).join(" ")||ge(e.username),ke(e)?a.password="plaintext":ge(e.password_digest)?a.password=pe(ge(e.password_hasher),ge(e.password_digest)):(a.password=Ie(t.passwords,[a.externalId,a.email.toLowerCase()],e=>pe(e.hasher,e.digest)),!a.password&&Array.isArray(e.email_addresses)&&!0===e.password_enabled&&r++),Array.isArray(e.external_accounts))for(const t of e.external_accounts.filter(me))a.links.push({provider:ge(t.provider).replace(/^oauth_/,""),id:t.provider_user_id??t.providerUserId});s.add(a,$e)}),r>0&&!t.passwords&&s.out.notes.push(`Clerk's Backend API leaves password hashes out, so ${r} user(s) with a password import without one. Export the CSV from the Clerk dashboard and pass it with --passwords to keep them.`),o>0&&0===n&&s.out.notes.push("Clerk's CSV export has no social sign-ins. To keep them, export users from the Backend API as JSON and pass this CSV with --passwords."),s.finish()}const xe={social:{"google-oauth2":"google",github:"github",windowslive:"microsoft",linkedin:"linkedIn",apple:"apple"},ignore:new Set(["auth0","email","sms"])};const Ae={social:{"google.com":"google","github.com":"github","microsoft.com":"microsoft","apple.com":"apple"},ignore:new Set(["password","phone","firebase","anonymous"])},Ue="--signer-key, --salt-separator, --rounds and --mem-cost";function Re(e,t={}){const s=e.filter(e=>ge(e.passwordHash)).length;if(s>0&&!t.firebase)throw new Error(`${s} user(s) in this export have a password hash. Pass the project's hash parameters with ${Ue} (Firebase console, Authentication, Users, the menu above the list, Password hash parameters).`);const n=new Se;return e.forEach((e,s)=>{const r={record:s+1,externalId:ge(e.localId),email:ge(e.email),emailVerified:we(e.emailVerified),name:ge(e.displayName),links:[],phone:_e(e.phoneNumber),phoneVerified:!0};if(ke(e)?r.password="plaintext":ge(e.passwordHash)&&t.firebase&&(r.password=function(e,t,s){const n=ae(e),r=ae(t);return le(n)&&le(r)?{ok:!0,password:{hasher:"firebase_scrypt",hash:n,salt:r,params:s}}:de("Firebase scrypt")}(ge(e.passwordHash),ge(e.salt),t.firebase)),Array.isArray(e.providerUserInfo))for(const t of e.providerUserInfo.filter(me))r.links.push({provider:ge(t.providerId),id:t.rawId});n.add(r,Ae)}),n.finish()}const Le={social:{google:"google",github:"github",azure:"microsoft",linkedin_oidc:"linkedIn",linkedin:"linkedIn",apple:"apple"},ignore:new Set(["email","phone","anonymous"])};function Ne(e){const t=_e(e);return/^[1-9]\d{6,14}$/.test(t)?`+${t}`:t}function Oe(e,t,s={}){switch(e){case"clerk":return Be(t,s);case"auth0":return function(e,t={}){const s=new Se;let n=0;return e.forEach((e,r)=>{const o=ge(e.user_id),i=ge(e.email),a=ge(e.name),l={record:r+1,externalId:o,email:i,emailVerified:we(e.email_verified),name:a&&a.toLowerCase()!==i.toLowerCase()?a:[ge(e.given_name),ge(e.family_name)].filter(Boolean).join(" "),links:[],phone:_e(e.phone_number),phoneVerified:we(e.phone_verified)},d=o.indexOf("|"),c=d>0?{provider:o.slice(0,d),id:o.slice(d+1)}:null,u=Array.isArray(e.identities)?e.identities.filter(me):[],p=u.length>0?u.map(e=>{const t=ge(e.provider),s=e.user_id;return{provider:t,id:"number"!=typeof s||Number.isSafeInteger(s)||c?.provider!==t?s:c.id}}):c?[c]:[];l.links=p;const h=p.some(e=>"auth0"===e.provider);if(ke(e))l.password="plaintext";else if(h){const e=p.find(e=>"auth0"===e.provider)?.id;l.password=Ie(t.passwords,[o,ge(e),ge(e)?`auth0|${ge(e)}`:"",i.toLowerCase()],e=>ce(e.digest)),l.password||(n++,t.passwords&&s.issue(l,{level:"warning",reason:"no_password_hash",message:"the --passwords file has no hash for this user, so they import without a password"}))}s.add(l,xe)}),n>0&&!t.passwords&&s.out.notes.push(`Auth0's bulk export has no password hashes, so ${n} database user(s) import without a password. Ask Auth0 support for the password hash export and pass it with --passwords, or those users set a password with Forgot password.`),s.finish()}(t,s);case"firebase":return Re(t,s);case"supabase":return function(e){const t=new Se;return e.forEach((e,s)=>{const n=ve(e.raw_user_meta_data),r={record:s+1,externalId:ge(e.id),email:ge(e.email),emailVerified:be(e.email_confirmed_at),name:me(n)?ge(n.full_name)||ge(n.name):"",links:[],phone:Ne(e.phone),phoneVerified:be(e.phone_confirmed_at)};if(be(e.deleted_at))return void t.issue(r,{level:"error",reason:"deleted",message:"the user is deleted in Supabase, so they are not imported"});ke(e)?r.password="plaintext":be(e.encrypted_password)&&(r.password=ce(ge(e.encrypted_password)));const o=ve(e.identities);if(Array.isArray(o))for(const e of o.filter(me)){const t=ve(e.identity_data);r.links.push({provider:ge(e.provider),id:be(e.provider_id)?e.provider_id:me(t)?t.sub??t.provider_id:void 0})}t.add(r,Le)}),t.finish()}(t,s)}}function Ce(e,t){if("auth0"===e)return function(e){const t=new Map;for(const s of e){const e=me(s._id)?ge(s._id.$oid):ge(s._id),n=ge(s.alt_id),r=ge(s.passwordHash)?{hasher:"bcrypt",digest:ge(s.passwordHash)}:ke(s)?{plaintext:!0}:null;if(!r)continue;const o=[n&&`auth0|${n}`,e&&`auth0|${e}`,n,e,ge(s.email).toLowerCase()];for(const e of o)e&&!t.has(e)&&t.set(e,r)}return t}(t);if("clerk"===e)return function(e){const t=new Map;for(const s of e){const e=ke(s)?{plaintext:!0}:ge(s.password_digest)?{hasher:ge(s.password_hasher),digest:ge(s.password_digest)}:null;if(!e)continue;const n=(ge(s.primary_email_address)||ge(s.email_address)).toLowerCase();for(const r of[ge(s.id),n])r&&!t.has(r)&&t.set(r,e)}return t}(t);throw new Error(`--passwords is for Auth0's hash export or Clerk's CSV, not ${e}`)}function Te(e){const[t,...s]=function(e){const t=[];let s=[],n="",r=!1;for(let o=0;o<e.length;o++){const i=e[o];r?'"'===i?'"'===e[o+1]?(n+='"',o++):r=!1:n+=i:'"'===i&&""===n?r=!0:","===i?(s.push(n),n=""):"\n"===i||"\r"===i?("\r"===i&&"\n"===e[o+1]&&o++,s.push(n),t.push(s),s=[],n=""):n+=i}if(r)throw new Error("the CSV ends inside a quoted field");return(""!==n||s.length>0)&&(s.push(n),t.push(s)),t.filter(e=>!(1===e.length&&""===e[0].trim()))}(e);if(!t)return[];const n=t.map(e=>e.trim());return s.map(e=>{const t={};return n.forEach((s,n)=>{s&&(t[s]=e[n]??"")}),t})}const De=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function je(e){if(Array.isArray(e)){if(1===e.length&&De(e[0])){const t=Object.values(e[0]);if(1===t.length&&Array.isArray(t[0]))return je(t[0]);if(1===t.length&&"string"==typeof t[0]&&t[0].trim().startsWith("["))return je(JSON.parse(t[0]))}return e.filter(De)}if(De(e)){for(const t of["users","data"])if(Array.isArray(e[t]))return e[t].filter(De);return[e]}throw new Error("the JSON holds no user records")}function Pe(e){const t=e.replace(/^/,""),s=t.trimStart();if(s.startsWith("[")||s.startsWith("{")){try{return{format:"json",records:je(JSON.parse(t))}}catch(e){if(!s.startsWith("{"))throw new Error(`not valid JSON: ${e.message}`)}const e=[];return t.split(/\r?\n/).forEach((t,s)=>{const n=t.trim();if(!n)return;let r;try{r=JSON.parse(n)}catch(e){throw new Error(`line ${s+1} is not JSON: ${e.message}`)}De(r)&&e.push(r)}),{format:"ndjson",records:e}}return{format:"csv",records:Te(t)}}function qe(e,t=500,s=45e5){const n=new TextEncoder,r=[];let o=[],i=[],a=0;const l=()=>{0!==o.length&&(r.push({users:o,ndjson:`${i.join("\n")}\n`}),o=[],i=[],a=0)};for(const r of e){const e=JSON.stringify(r.line),d=n.encode(e).length+1;(o.length>=t||o.length>0&&a+d>s)&&l(),o.push(r),i.push(e),a+=d}return l(),r}class Ve extends Error{constructor(e,t,s){super(e),this.status=t,this.report=s,this.name="ImportRequestError"}}const Fe=e=>"number"==typeof e&&Number.isFinite(e)?Math.max(0,Math.floor(e)):0;function We(e){const t=e&&"object"==typeof e&&"data"in e&&"success"in e?e.data:e,s=t&&"object"==typeof t?t:{};return{total:Fe(s.total),created:Fe(s.created),updated:Fe(s.updated),skipped:Fe(s.skipped),failed:Fe(s.failed),rows:Array.isArray(s.rows)?s.rows:[],notices:Array.isArray(s.notices)?s.notices.filter(e=>"string"==typeof e&&""!==e):[]}}const Ke=/^[A-Za-z0-9_-]{1,40}$/,ze=e=>"string"==typeof e&&e?e:void 0;function Me(e){const t=e.email?` (${e.email})`:"",s=e.reason?`${e.reason}: `:"";return`record ${e.record}${t}: ${s}${e.message??""}`.trimEnd()}function Je(e){if(!e.password)return JSON.stringify(e);const t="firebase_scrypt"===e.password.hasher?{hasher:e.password.hasher,hash:"<redacted>",salt:"<redacted>",params:{signerKey:"<redacted>",saltSeparator:"<redacted>",rounds:e.password.params.rounds,memCost:e.password.params.memCost}}:{hasher:e.password.hasher,hash:"<redacted>"};return JSON.stringify({...e,password:t})}async function He(e,t){let s=await e.readFile(t);return s.length>2&&31===s[0]&&139===s[1]&&(s=e.gunzip(s)),(new TextDecoder).decode(s)}const Xe=(e,t,s=`${t}s`)=>`${e} ${1===e?t:s}`;function Ge(e,t,s){if(0!==s.length){e.log(`\n${t}:`);for(const t of[...s].sort((e,t)=>e.record-t.record))e.log(` ${Me(t)}`)}}function Ze(e,t){for(const s of t)e.log(`note: ${s}`)}function Ye(e,t,s){e.log(`buildbase: ${t.created} created, ${t.updated} updated, ${t.skipped} skipped, ${t.failed} failed (${Xe(t.sent,"user")} sent${s?`, ${s} not sent`:""})`)}async function Qe(e,t){let s;try{const{format:n,records:r}=Pe(await He(t,e.file));if("firebase"===e.from&&"csv"===n)throw new Error("read the JSON export: firebase auth:export users.json --format=JSON (the CSV has no Microsoft or Apple sign-ins)");if(0===r.length)throw new Error(`${e.file} holds no user records`);let o;if(e.passwords){const s=Pe(await He(t,e.passwords));if(o=Ce(e.from,s.records),0===o.size)throw new Error(`${e.passwords} holds no password hashes`)}const i=e.firebase,a=[i.signerKey,i.saltSeparator,i.rounds,i.memCost].some(Boolean),l="firebase"===e.from?function(e){const t=[e.signerKey,e.saltSeparator,e.rounds,e.memCost];if(t.every(e=>void 0===e))return;if(t.some(e=>void 0===e||""===e))throw new Error(`Firebase hashes need all four of ${Ue}`);const s=ae(e.signerKey),n=ae(e.saltSeparator);if(!le(s))throw new Error("--signer-key must be the base64 signer key from the Firebase console");if(!le(n))throw new Error("--salt-separator must be the base64 salt separator from the Firebase console");const r=Number(e.rounds),o=Number(e.memCost);if(!Number.isInteger(r)||r<1)throw new Error("--rounds must be a positive whole number");if(!Number.isInteger(o)||o<1)throw new Error("--mem-cost must be a positive whole number");return{signerKey:s,saltSeparator:n,rounds:r,memCost:o}}({...i,signerKey:i.signerKey??(a?t.env.BUILDBASE_FIREBASE_SIGNER_KEY:void 0)}):void 0;s=Oe(e.from,r,{passwords:o,firebase:l}),t.log(`buildbase: read ${Xe(r.length,"record")} from ${e.file} (${e.from}): ${s.users.length} to import`)}catch(e){return t.error(`buildbase: ${e.message}`),2}const n=s.issues.filter(e=>"error"===e.level),r=s.issues.filter(e=>"warning"===e.level);for(const n of s.notes)!e.dryRun&&fe(n)||t.log(`note: ${n}`);if(e.dryRun){const e=s.users.slice(0,3);if(e.length){t.log(`\nThe first ${Xe(e.length,"user")}, as they would be sent (hashes redacted):`);for(const s of e)t.log(` record ${s.record}: ${Je(s.line)}`)}return Ge(t,"Not sent",n),Ge(t,"Warnings",r),t.log(`\nbuildbase: dry run, nothing was sent. ${Xe(s.users.length,"user")} would be sent in ${Xe(qe(s.users).length,"batch","batches")}.`),0}const o=t.env.BUILDBASE_API_TOKEN,i=e.serverUrl??t.env.BUILDBASE_SERVER_URL??t.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL;if(!o)return t.error("buildbase: set BUILDBASE_API_TOKEN to an org API token allowed to create users"),2;if(!i)return t.error("buildbase: set BUILDBASE_SERVER_URL (for example https://api.console.buildbase.app)"),2;if(0===s.users.length)return Ge(t,"Not sent",n),Ge(t,"Warnings",r),t.error("buildbase: nothing to send"),1;const a=qe(s.users);let l;try{l=await async function(e,t){const s=t.fetch??((e,t)=>globalThis.fetch(e,t)),n=`${t.serverUrl.replace(/\/+$/,"")}/api/users/import`,r={sent:0,created:0,updated:0,skipped:0,failed:0,failedRows:[],warningRows:[],notices:[],batchesDone:0,batchesTotal:e.length};for(const o of e){const i=new FormData;i.append("file",new Blob([o.ndjson],{type:"application/x-ndjson"}),"users.ndjson"),t.overwrite&&i.append("overwrite","true"),t.source&&Ke.test(t.source)&&i.append("source",t.source);const a=new AbortController,l=setTimeout(()=>a.abort(),t.timeoutMs??3e5);let d;try{d=await s(n,{method:"POST",headers:{Authorization:`Bearer ${t.apiToken}`,Accept:"application/json"},body:i,signal:a.signal})}catch(e){throw new Ve(`the request failed: ${e instanceof Error?e.message:String(e)}`,0,r)}finally{clearTimeout(l)}const c=await d.text().catch(()=>"");let u;try{u=c?JSON.parse(c):void 0}catch{u=void 0}if(!d.ok){const e=ze(u?.message);throw new Ve(e??`the server answered ${d.status}${c&&!u?`: ${c.slice(0,200)}`:""}`,d.status,r)}const p=We(u);r.sent+=o.users.length,r.created+=p.created,r.updated+=p.updated,r.skipped+=p.skipped,r.failed+=p.failed;for(const e of p.rows){const t=o.users[Fe(e.line)-1],s={record:t?.record??0,email:ze(e.email)??t?.line.email,reason:ze(e.reason),message:ze(e.message)};"failed"===e.status&&r.failedRows.push(s),Array.isArray(e.warnings)&&e.warnings.length>0&&r.warningRows.push({...s,reason:void 0,message:e.warnings.map(e=>"string"==typeof e?e:ze(e?.message)??JSON.stringify(e)).join("; ")})}for(const e of p.notices)r.notices.includes(e)||r.notices.push(e);r.batchesDone++,t.onBatch?.(r.batchesDone,e.length)}return r}(a,{serverUrl:i,apiToken:o,overwrite:e.overwrite,source:e.from,fetch:t.fetch,onBatch:(e,s)=>{s>1&&t.log(`buildbase: batch ${e} of ${s} done`)}})}catch(o){if(!(o instanceof Ve))throw o;const i=401===o.status||403===o.status;t.error(i?`buildbase: the API token was refused (${o.status}). Check BUILDBASE_API_TOKEN and that its role can create users.`:`buildbase: batch ${o.report.batchesDone+1} of ${o.report.batchesTotal} was not imported: ${o.message}`);const a=s.users.length-o.report.sent;return o.report.sent>0&&(Ye(t,o.report,n.length+a),Ze(t,o.report.notices),Ge(t,"Not sent",n),Ge(t,"Failed",o.report.failedRows),Ge(t,"Warnings",[...r,...o.report.warningRows])),!i&&a>0&&t.error(`buildbase: ${Xe(a,"user")} were not sent. Run the same command again: users already imported are ${e.overwrite?"updated again":"skipped"}.`),1}return t.log(""),Ye(t,l,n.length),Ze(t,l.notices),Ge(t,"Not sent",n),Ge(t,"Failed",l.failedRows),Ge(t,"Warnings",[...r,...l.warningRows]),n.length+l.failed>0?1:0}(async function(c){let u;try{u=B(c)}catch(e){return console.error(`buildbase: ${e.message}\n\n${$(c)}`),2}if("help"===u.command)return console.log($(c)),0;if("init"===u.command)return V(u,{cwd:process.cwd(),isTTY:Boolean(process.stdin.isTTY&&process.stdout.isTTY),env:process.env,fs:{exists:e=>o(e).then(()=>!0,()=>!1),readFile:e=>r(e,"utf8"),writeFile:(e,t)=>s(e,t,"utf8"),mkdir:e=>t(e,{recursive:!0}).then(()=>{}),readdir:e=>n(e)},run:(t,s)=>new Promise(n=>{const r=e(t,s,{stdio:"inherit",cwd:process.cwd(),shell:"win32"===process.platform});r.on("error",e=>{console.error(`buildbase: could not run ${t}: ${e.message}`),n(1)}),r.on("close",e=>n(e??1))}),prompt:async e=>{const t=l({input:process.stdin,output:process.stdout});try{return await t.question(e)}finally{t.close()}},log:e=>console.log(e),error:e=>console.error(e)});if("import"===u.command)return Qe(u,{env:process.env,readFile:e=>r(i(process.cwd(),e)),gunzip:e=>d(e),log:e=>console.log(e),error:e=>console.error(e)});const p=process.env.BUILDBASE_API_TOKEN,h=u.serverUrl??process.env.BUILDBASE_SERVER_URL??process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL;if(!p)return console.error("buildbase: set BUILDBASE_API_TOKEN to an org API token (<orgId>:<secret>)"),2;if(!h)return console.error("buildbase: set BUILDBASE_SERVER_URL (for example https://api.console.buildbase.app)"),2;try{const e=te({serverUrl:h,apiToken:p,cache:!1}),n=await async function(e,t={}){const s=new Set(t.modules??g),n=t.pageSize??100,r={generatedAt:(new Date).toISOString()};if(s.has("blogs")&&(r.blogs=await w(t=>e.blogs.list({page:t,limit:n}))),s.has("docs")&&(r.docs=await w(t=>e.docs.list({page:t,limit:n}))),s.has("faqs")){const t=await w(t=>e.faqs.collections({page:t,limit:n}));r.faqs=[];for(const s of t)r.faqs.push({...s,questions:await e.faqs.questions(s._id)})}if(s.has("testimonials")&&(r.testimonials=await w(t=>e.testimonials.list({page:t,limit:n}))),s.has("richContent")&&(r.richContent=await w(t=>e.richContent.list({page:t,limit:n}))),s.has("collections")){const s=t.collections??(await w(t=>e.collections.list({page:t,limit:n}))).map(e=>e.slug);r.collections={};for(const t of s)r.collections[t]=await e.collections.data(t)}return r}(e,{modules:u.modules,collections:u.collections,pageSize:u.pageSize}),r=i(process.cwd(),u.out);await t(a(r),{recursive:!0}),await s(r,`${JSON.stringify(n,null,2)}\n`,"utf8");const o=Object.entries(n).filter(([e])=>"generatedAt"!==e).map(([e,t])=>`${e}: ${Array.isArray(t)?t.length:Object.keys(t).length}`).join(", ");return console.log(`buildbase: wrote ${u.out} (${o})`),0}catch(e){return e instanceof X&&(401===e.status||403===e.status)?(console.error(`buildbase: the API token was refused (${e.status}). Check BUILDBASE_API_TOKEN and that its role can read content.`),1):(console.error(`buildbase: ${e.message}`),1)}})(process.argv.slice(2)).then(e=>process.exit(e),e=>{console.error(e),process.exit(1)});
|