@brokr/sdk 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account.js +34 -0
- package/dist/account.mjs +7 -0
- package/dist/auth.js +628 -114
- package/dist/auth.mjs +611 -111
- package/dist/chat.js +34 -0
- package/dist/chat.mjs +7 -0
- package/dist/events.js +64 -0
- package/dist/events.mjs +37 -0
- package/dist/feature.js +6304 -0
- package/dist/feature.mjs +6278 -0
- package/dist/files.js +428 -0
- package/dist/files.mjs +408 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4069 -454
- package/dist/index.mjs +4040 -448
- package/dist/logs.js +148 -0
- package/dist/logs.mjs +124 -0
- package/dist/management.js +14 -13
- package/dist/management.mjs +14 -13
- package/dist/next.js +2725 -0
- package/dist/next.mjs +2710 -0
- package/dist/notifications.js +140 -0
- package/dist/notifications.mjs +110 -0
- package/dist/payments.js +32 -0
- package/dist/payments.mjs +7 -0
- package/dist/react-notifications.js +299 -0
- package/dist/react-notifications.mjs +267 -0
- package/dist/react-styles.js +2756 -0
- package/dist/react-styles.mjs +2720 -0
- package/dist/react-theme.js +4196 -0
- package/dist/react-theme.mjs +4172 -0
- package/dist/react.js +8591 -209
- package/dist/react.mjs +8571 -183
- package/dist/runtime.js +2113 -385
- package/dist/runtime.mjs +2085 -397
- package/dist/src/account/config.d.ts +42 -0
- package/dist/src/account/config.d.ts.map +1 -0
- package/dist/src/account/index.d.ts +3 -0
- package/dist/src/account/index.d.ts.map +1 -0
- package/dist/src/ai/client.d.ts +58 -0
- package/dist/src/ai/client.d.ts.map +1 -0
- package/dist/src/ai/conversation-title.d.ts +13 -0
- package/dist/src/ai/conversation-title.d.ts.map +1 -0
- package/dist/src/ai/types.d.ts +81 -0
- package/dist/src/ai/types.d.ts.map +1 -0
- package/dist/src/auth.d.ts +133 -20
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/chat/config.d.ts +61 -0
- package/dist/src/chat/config.d.ts.map +1 -0
- package/dist/src/chat/index.d.ts +3 -0
- package/dist/src/chat/index.d.ts.map +1 -0
- package/dist/src/chat/sse-parser.d.ts +44 -0
- package/dist/src/chat/sse-parser.d.ts.map +1 -0
- package/dist/src/dev-console.d.ts +18 -0
- package/dist/src/dev-console.d.ts.map +1 -0
- package/dist/src/email/client.d.ts +33 -0
- package/dist/src/email/client.d.ts.map +1 -0
- package/dist/src/email/templates.d.ts +15 -0
- package/dist/src/email/templates.d.ts.map +1 -0
- package/dist/src/email/types.d.ts +35 -0
- package/dist/src/email/types.d.ts.map +1 -0
- package/dist/src/env-detect.d.ts +25 -0
- package/dist/src/env-detect.d.ts.map +1 -0
- package/dist/src/errors.d.ts +53 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/events/client.d.ts +25 -0
- package/dist/src/events/client.d.ts.map +1 -0
- package/dist/src/events/index.d.ts +9 -0
- package/dist/src/events/index.d.ts.map +1 -0
- package/dist/src/events/types.d.ts +10 -0
- package/dist/src/events/types.d.ts.map +1 -0
- package/dist/src/feature/canonical.d.ts +33 -0
- package/dist/src/feature/canonical.d.ts.map +1 -0
- package/dist/src/feature/create-feature.d.ts +33 -0
- package/dist/src/feature/create-feature.d.ts.map +1 -0
- package/dist/src/feature/db.d.ts +16 -0
- package/dist/src/feature/db.d.ts.map +1 -0
- package/dist/src/feature/handlers.d.ts +28 -0
- package/dist/src/feature/handlers.d.ts.map +1 -0
- package/dist/src/feature/index.d.ts +21 -0
- package/dist/src/feature/index.d.ts.map +1 -0
- package/dist/src/feature/manifest.d.ts +173 -0
- package/dist/src/feature/manifest.d.ts.map +1 -0
- package/dist/src/feature/mapping.d.ts +18 -0
- package/dist/src/feature/mapping.d.ts.map +1 -0
- package/dist/src/feature/runtime.d.ts +45 -0
- package/dist/src/feature/runtime.d.ts.map +1 -0
- package/dist/src/feature/types.d.ts +65 -0
- package/dist/src/feature/types.d.ts.map +1 -0
- package/dist/src/files/client.d.ts +28 -0
- package/dist/src/files/client.d.ts.map +1 -0
- package/dist/src/files/types.d.ts +28 -0
- package/dist/src/files/types.d.ts.map +1 -0
- package/dist/src/fix-registry.d.ts +8 -0
- package/dist/src/fix-registry.d.ts.map +1 -0
- package/dist/src/gateway.d.ts +32 -0
- package/dist/src/gateway.d.ts.map +1 -0
- package/dist/src/logs/capture.d.ts +56 -0
- package/dist/src/logs/capture.d.ts.map +1 -0
- package/dist/src/logs/index.d.ts +2 -0
- package/dist/src/logs/index.d.ts.map +1 -0
- package/dist/src/management.d.ts +1 -1
- package/dist/src/management.d.ts.map +1 -1
- package/dist/src/models.d.ts +32 -0
- package/dist/src/models.d.ts.map +1 -0
- package/dist/src/next/auth.d.ts +54 -0
- package/dist/src/next/auth.d.ts.map +1 -0
- package/dist/src/next/chat.d.ts +31 -0
- package/dist/src/next/chat.d.ts.map +1 -0
- package/dist/src/next/index.d.ts +14 -0
- package/dist/src/next/index.d.ts.map +1 -0
- package/dist/src/next/notifications.d.ts +67 -0
- package/dist/src/next/notifications.d.ts.map +1 -0
- package/dist/src/notifications/built-ins.d.ts +9 -0
- package/dist/src/notifications/built-ins.d.ts.map +1 -0
- package/dist/src/notifications/client.d.ts +38 -0
- package/dist/src/notifications/client.d.ts.map +1 -0
- package/dist/src/notifications/config.d.ts +71 -0
- package/dist/src/notifications/config.d.ts.map +1 -0
- package/dist/src/notifications/index.d.ts +6 -0
- package/dist/src/notifications/index.d.ts.map +1 -0
- package/dist/src/notifications/registry.d.ts +67 -0
- package/dist/src/notifications/registry.d.ts.map +1 -0
- package/dist/src/notifications/types.d.ts +48 -0
- package/dist/src/notifications/types.d.ts.map +1 -0
- package/dist/src/payments/client.d.ts +64 -0
- package/dist/src/payments/client.d.ts.map +1 -0
- package/dist/src/payments/config.d.ts +46 -0
- package/dist/src/payments/config.d.ts.map +1 -0
- package/dist/src/payments/entitlements.d.ts +48 -0
- package/dist/src/payments/entitlements.d.ts.map +1 -0
- package/dist/src/payments/types.d.ts +135 -0
- package/dist/src/payments/types.d.ts.map +1 -0
- package/dist/src/react/BrokrErrorBoundary.d.ts +23 -0
- package/dist/src/react/BrokrErrorBoundary.d.ts.map +1 -0
- package/dist/src/react/account/AccountPanel.d.ts +12 -0
- package/dist/src/react/account/AccountPanel.d.ts.map +1 -0
- package/dist/src/react/account/Avatar.d.ts +11 -0
- package/dist/src/react/account/Avatar.d.ts.map +1 -0
- package/dist/src/react/account/ProfilePhotoButton.d.ts +7 -0
- package/dist/src/react/account/ProfilePhotoButton.d.ts.map +1 -0
- package/dist/src/react/account/UserButton.d.ts +7 -0
- package/dist/src/react/account/UserButton.d.ts.map +1 -0
- package/dist/src/react/auth-pages/AuthPageShell.d.ts +9 -0
- package/dist/src/react/auth-pages/AuthPageShell.d.ts.map +1 -0
- package/dist/src/react/auth-pages/SignInPage.d.ts +9 -0
- package/dist/src/react/auth-pages/SignInPage.d.ts.map +1 -0
- package/dist/src/react/auth-pages/SignUpPage.d.ts +8 -0
- package/dist/src/react/auth-pages/SignUpPage.d.ts.map +1 -0
- package/dist/src/react/auth.d.ts +1 -49
- package/dist/src/react/auth.d.ts.map +1 -1
- package/dist/src/react/chat/AIChat.d.ts +4 -0
- package/dist/src/react/chat/AIChat.d.ts.map +1 -0
- package/dist/src/react/chat/ChatContext.d.ts +76 -0
- package/dist/src/react/chat/ChatContext.d.ts.map +1 -0
- package/dist/src/react/chat/ChatInput.d.ts +3 -0
- package/dist/src/react/chat/ChatInput.d.ts.map +1 -0
- package/dist/src/react/chat/MarkdownRenderer.d.ts +5 -0
- package/dist/src/react/chat/MarkdownRenderer.d.ts.map +1 -0
- package/dist/src/react/chat/MessageBubble.d.ts +14 -0
- package/dist/src/react/chat/MessageBubble.d.ts.map +1 -0
- package/dist/src/react/chat/MessagePane.d.ts +10 -0
- package/dist/src/react/chat/MessagePane.d.ts.map +1 -0
- package/dist/src/react/chat/ModelSelector.d.ts +13 -0
- package/dist/src/react/chat/ModelSelector.d.ts.map +1 -0
- package/dist/src/react/chat/ThreadSidebar.d.ts +3 -0
- package/dist/src/react/chat/ThreadSidebar.d.ts.map +1 -0
- package/dist/src/react/chat/index.d.ts +5 -0
- package/dist/src/react/chat/index.d.ts.map +1 -0
- package/dist/src/react/chat/token-limit.d.ts +14 -0
- package/dist/src/react/chat/token-limit.d.ts.map +1 -0
- package/dist/src/react/chat/types.d.ts +65 -0
- package/dist/src/react/chat/types.d.ts.map +1 -0
- package/dist/src/react/chat/useChat.d.ts +57 -0
- package/dist/src/react/chat/useChat.d.ts.map +1 -0
- package/dist/src/react/composites/FabAI.d.ts +15 -0
- package/dist/src/react/composites/FabAI.d.ts.map +1 -0
- package/dist/src/react/composites/FeedbackWidget.d.ts +10 -0
- package/dist/src/react/composites/FeedbackWidget.d.ts.map +1 -0
- package/dist/src/react/composites/SmartUpload.d.ts +12 -0
- package/dist/src/react/composites/SmartUpload.d.ts.map +1 -0
- package/dist/src/react/config.d.ts +23 -0
- package/dist/src/react/config.d.ts.map +1 -0
- package/dist/src/react/context.d.ts +4 -0
- package/dist/src/react/context.d.ts.map +1 -0
- package/dist/src/react/css/account.d.ts +2 -0
- package/dist/src/react/css/account.d.ts.map +1 -0
- package/dist/src/react/css/animations.d.ts +2 -0
- package/dist/src/react/css/animations.d.ts.map +1 -0
- package/dist/src/react/css/auth.d.ts +2 -0
- package/dist/src/react/css/auth.d.ts.map +1 -0
- package/dist/src/react/css/chat-extras.d.ts +2 -0
- package/dist/src/react/css/chat-extras.d.ts.map +1 -0
- package/dist/src/react/css/chat.d.ts +2 -0
- package/dist/src/react/css/chat.d.ts.map +1 -0
- package/dist/src/react/css/composites.d.ts +2 -0
- package/dist/src/react/css/composites.d.ts.map +1 -0
- package/dist/src/react/css/gates.d.ts +2 -0
- package/dist/src/react/css/gates.d.ts.map +1 -0
- package/dist/src/react/css/index.d.ts +3 -0
- package/dist/src/react/css/index.d.ts.map +1 -0
- package/dist/src/react/css/markdown.d.ts +2 -0
- package/dist/src/react/css/markdown.d.ts.map +1 -0
- package/dist/src/react/css/notifications.d.ts +2 -0
- package/dist/src/react/css/notifications.d.ts.map +1 -0
- package/dist/src/react/css/primitives.d.ts +2 -0
- package/dist/src/react/css/primitives.d.ts.map +1 -0
- package/dist/src/react/css/reset.d.ts +2 -0
- package/dist/src/react/css/reset.d.ts.map +1 -0
- package/dist/src/react/css/responsive.d.ts +2 -0
- package/dist/src/react/css/responsive.d.ts.map +1 -0
- package/dist/src/react/css/skeleton.d.ts +2 -0
- package/dist/src/react/css/skeleton.d.ts.map +1 -0
- package/dist/src/react/css/tokens.d.ts +2 -0
- package/dist/src/react/css/tokens.d.ts.map +1 -0
- package/dist/src/react/gates/AuthWall.d.ts +7 -0
- package/dist/src/react/gates/AuthWall.d.ts.map +1 -0
- package/dist/src/react/gates/BillingBoundary.d.ts +4 -0
- package/dist/src/react/gates/BillingBoundary.d.ts.map +1 -0
- package/dist/src/react/gates/Gate.d.ts +9 -0
- package/dist/src/react/gates/Gate.d.ts.map +1 -0
- package/dist/src/react/gates/RequirePlan.d.ts +4 -0
- package/dist/src/react/gates/RequirePlan.d.ts.map +1 -0
- package/dist/src/react/gates/RequireUser.d.ts +4 -0
- package/dist/src/react/gates/RequireUser.d.ts.map +1 -0
- package/dist/src/react/gates/UsageGate.d.ts +4 -0
- package/dist/src/react/gates/UsageGate.d.ts.map +1 -0
- package/dist/src/react/helpers.d.ts +7 -0
- package/dist/src/react/helpers.d.ts.map +1 -0
- package/dist/src/react/hooks/use-theme.d.ts +15 -0
- package/dist/src/react/hooks/use-theme.d.ts.map +1 -0
- package/dist/src/react/hooks/use-user.d.ts +12 -0
- package/dist/src/react/hooks/use-user.d.ts.map +1 -0
- package/dist/src/react/icons.d.ts +26 -0
- package/dist/src/react/icons.d.ts.map +1 -0
- package/dist/src/react/index.d.ts +48 -0
- package/dist/src/react/index.d.ts.map +1 -0
- package/dist/src/react/notifications/NotificationBell.d.ts +7 -0
- package/dist/src/react/notifications/NotificationBell.d.ts.map +1 -0
- package/dist/src/react/notifications/NotificationList.d.ts +7 -0
- package/dist/src/react/notifications/NotificationList.d.ts.map +1 -0
- package/dist/src/react/notifications/Toast.d.ts +13 -0
- package/dist/src/react/notifications/Toast.d.ts.map +1 -0
- package/dist/src/react/notifications/index.d.ts +8 -0
- package/dist/src/react/notifications/index.d.ts.map +1 -0
- package/dist/src/react/notifications/provider.d.ts +14 -0
- package/dist/src/react/notifications/provider.d.ts.map +1 -0
- package/dist/src/react/notifications/use-notifications.d.ts +24 -0
- package/dist/src/react/notifications/use-notifications.d.ts.map +1 -0
- package/dist/src/react/payments/AutoReloadToggle.d.ts +6 -0
- package/dist/src/react/payments/AutoReloadToggle.d.ts.map +1 -0
- package/dist/src/react/payments/Balance.d.ts +8 -0
- package/dist/src/react/payments/Balance.d.ts.map +1 -0
- package/dist/src/react/payments/CancelSubscription.d.ts +6 -0
- package/dist/src/react/payments/CancelSubscription.d.ts.map +1 -0
- package/dist/src/react/payments/CheckoutButton.d.ts +7 -0
- package/dist/src/react/payments/CheckoutButton.d.ts.map +1 -0
- package/dist/src/react/payments/CustomerPortalButton.d.ts +6 -0
- package/dist/src/react/payments/CustomerPortalButton.d.ts.map +1 -0
- package/dist/src/react/payments/FeatureMeter.d.ts +6 -0
- package/dist/src/react/payments/FeatureMeter.d.ts.map +1 -0
- package/dist/src/react/payments/Plans.d.ts +8 -0
- package/dist/src/react/payments/Plans.d.ts.map +1 -0
- package/dist/src/react/payments/TopUpButton.d.ts +8 -0
- package/dist/src/react/payments/TopUpButton.d.ts.map +1 -0
- package/dist/src/react/payments/UpdateBilling.d.ts +3 -0
- package/dist/src/react/payments/UpdateBilling.d.ts.map +1 -0
- package/dist/src/react/payments/UpgradePrompt.d.ts +8 -0
- package/dist/src/react/payments/UpgradePrompt.d.ts.map +1 -0
- package/dist/src/react/primitives/Skeleton.d.ts +15 -0
- package/dist/src/react/primitives/Skeleton.d.ts.map +1 -0
- package/dist/src/react/provider.d.ts +21 -0
- package/dist/src/react/provider.d.ts.map +1 -0
- package/dist/src/react/request.d.ts +2 -0
- package/dist/src/react/request.d.ts.map +1 -0
- package/dist/src/react/styles-entry.d.ts +4 -0
- package/dist/src/react/styles-entry.d.ts.map +1 -0
- package/dist/src/react/styles.d.ts +2 -0
- package/dist/src/react/styles.d.ts.map +1 -0
- package/dist/src/react/theme-entry.d.ts +3 -0
- package/dist/src/react/theme-entry.d.ts.map +1 -0
- package/dist/src/react/theme.d.ts +6 -0
- package/dist/src/react/theme.d.ts.map +1 -0
- package/dist/src/react/types.d.ts +191 -0
- package/dist/src/react/types.d.ts.map +1 -0
- package/dist/src/react/use-brokr-theme.d.ts +6 -0
- package/dist/src/react/use-brokr-theme.d.ts.map +1 -0
- package/dist/src/runtime.d.ts +69 -180
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/storage/client.d.ts +113 -0
- package/dist/src/storage/client.d.ts.map +1 -0
- package/dist/src/storage/types.d.ts +60 -0
- package/dist/src/storage/types.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +70 -9
package/dist/runtime.mjs
CHANGED
|
@@ -1,166 +1,275 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
1
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
2
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
3
|
}) : x)(function(x) {
|
|
8
4
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
6
|
});
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
|
|
8
|
+
// src/fix-registry.ts
|
|
9
|
+
var FIX_REGISTRY = {
|
|
10
|
+
AUTH_TOKEN_INVALID: [
|
|
11
|
+
"\u2192 Run `brokr env pull --stack <name>` to refresh your token",
|
|
12
|
+
"\u2192 Or run `brokr link account` to re-authenticate"
|
|
13
|
+
].join("\n"),
|
|
14
|
+
AUTH_SESSION_EXPIRED: [
|
|
15
|
+
"\u2192 Run `brokr link account` to re-authenticate"
|
|
16
|
+
].join("\n"),
|
|
17
|
+
BROKR_TOKEN_MISSING: [
|
|
18
|
+
"\u2192 Run `brokr env pull --stack <name>` to sync your environment",
|
|
19
|
+
"\u2192 Or add BROKR_TOKEN to your .env.local manually"
|
|
20
|
+
].join("\n"),
|
|
21
|
+
DATABASE_PROVISION_FAILED: [
|
|
22
|
+
"\u2192 Run `brokr status <stack>` to check current state",
|
|
23
|
+
"\u2192 Run `brokr retry <stack>` to re-trigger provisioning"
|
|
24
|
+
].join("\n"),
|
|
25
|
+
DATABASE_QUOTA_REACHED: [
|
|
26
|
+
"\u2192 Upgrade your plan or delete unused databases",
|
|
27
|
+
"\u2192 Check usage at brokr.sh/billing"
|
|
28
|
+
].join("\n"),
|
|
29
|
+
DATABASE_HEALTH_TIMEOUT: [
|
|
30
|
+
"\u2192 Run `brokr status <stack>` to check database state",
|
|
31
|
+
"\u2192 If persistent, check provider status page"
|
|
32
|
+
].join("\n"),
|
|
33
|
+
DEPLOYMENT_FAILED: [
|
|
34
|
+
"\u2192 Run `brokr logs --stack <name>` to see build logs",
|
|
35
|
+
"\u2192 Fix the build error and run `brokr deploy`"
|
|
36
|
+
].join("\n"),
|
|
37
|
+
DEPLOYMENT_RATE_LIMITED: [
|
|
38
|
+
"\u2192 Wait a minute and retry",
|
|
39
|
+
"\u2192 Vercel rate limits apply per project"
|
|
40
|
+
].join("\n"),
|
|
41
|
+
REPO_ALREADY_EXISTS: [
|
|
42
|
+
"\u2192 Use a different stack name",
|
|
43
|
+
"\u2192 Or run `brokr status <stack>` to check the existing stack"
|
|
44
|
+
].join("\n"),
|
|
45
|
+
REPO_CREATE_FAILED: [
|
|
46
|
+
"\u2192 Check your GitHub connection: `brokr link account`",
|
|
47
|
+
"\u2192 Ensure the Brokr GitHub App is installed"
|
|
48
|
+
].join("\n"),
|
|
49
|
+
EMAIL_DOMAIN_FAILED: [
|
|
50
|
+
"\u2192 Check DNS records are propagated",
|
|
51
|
+
"\u2192 Run `brokr status <stack>` for details"
|
|
52
|
+
].join("\n"),
|
|
53
|
+
DNS_RECORD_FAILED: [
|
|
54
|
+
"\u2192 Check Cloudflare dashboard for zone status",
|
|
55
|
+
"\u2192 Retry with `brokr retry <stack>`"
|
|
56
|
+
].join("\n"),
|
|
57
|
+
INSUFFICIENT_CREDITS: [
|
|
58
|
+
"\u2192 Top up credits at brokr.sh/billing",
|
|
59
|
+
"\u2192 Or run `brokr billing topup`"
|
|
60
|
+
].join("\n"),
|
|
61
|
+
AI_BUDGET_EXCEEDED: [
|
|
62
|
+
"\u2192 Top up credits at brokr.sh/billing",
|
|
63
|
+
"\u2192 Or run `brokr billing topup`"
|
|
64
|
+
].join("\n"),
|
|
65
|
+
AI_PROVIDER_UNAVAILABLE: [
|
|
66
|
+
"\u2192 The AI provider is temporarily down",
|
|
67
|
+
"\u2192 Retry in a few seconds \u2014 this is usually transient"
|
|
68
|
+
].join("\n"),
|
|
69
|
+
AI_MODEL_NOT_FOUND: [
|
|
70
|
+
"\u2192 Check the model name in your configuration",
|
|
71
|
+
"\u2192 See available models at brokr.sh/docs/ai-models"
|
|
72
|
+
].join("\n"),
|
|
73
|
+
ENV_VAR_MISSING: [
|
|
74
|
+
"\u2192 Run `brokr env pull --stack <name>` to sync environment",
|
|
75
|
+
"\u2192 Check required vars in your template README"
|
|
76
|
+
].join("\n"),
|
|
77
|
+
STACK_LIMIT_REACHED: [
|
|
78
|
+
"\u2192 Delete unused stacks with `brokr delete <stack>`",
|
|
79
|
+
"\u2192 Or upgrade your plan at brokr.sh/billing"
|
|
80
|
+
].join("\n"),
|
|
81
|
+
STACK_NOT_FOUND: [
|
|
82
|
+
"\u2192 Check the stack name: `brokr list`",
|
|
83
|
+
"\u2192 Create a new stack: `brokr create --name <name>`"
|
|
84
|
+
].join("\n"),
|
|
85
|
+
RATE_LIMITED: [
|
|
86
|
+
"\u2192 Wait a moment and retry",
|
|
87
|
+
"\u2192 If persistent, check brokr.sh/status"
|
|
88
|
+
].join("\n"),
|
|
89
|
+
GATEWAY_AUTH_FAILED: [
|
|
90
|
+
"\u2192 Your BROKR_TOKEN may be expired",
|
|
91
|
+
"\u2192 Run `brokr env pull --stack <name>` to refresh"
|
|
92
|
+
].join("\n"),
|
|
93
|
+
BROKR_TOKEN_INVALID: [
|
|
94
|
+
"\u2192 Your BROKR_TOKEN has expired or been revoked",
|
|
95
|
+
"\u2192 Run `brokr env pull --stack <name>` to get a fresh token"
|
|
96
|
+
].join("\n"),
|
|
97
|
+
AI_STREAM_ERROR: [
|
|
98
|
+
"\u2192 The AI stream was interrupted",
|
|
99
|
+
"\u2192 Retry the request \u2014 this is usually transient"
|
|
100
|
+
].join("\n"),
|
|
101
|
+
EMAIL_SEND_FAILED: [
|
|
102
|
+
"\u2192 Email delivery failed",
|
|
103
|
+
"\u2192 Check that your domain DNS is verified: `brokr status <stack>`"
|
|
104
|
+
].join("\n"),
|
|
105
|
+
EMAIL_NOT_CONFIGURED: [
|
|
106
|
+
"\u2192 Email is not set up for this stack",
|
|
107
|
+
"\u2192 Add email capability: `brokr add email --stack <name>`"
|
|
108
|
+
].join("\n"),
|
|
109
|
+
EMAIL_INVALID_FROM: [
|
|
110
|
+
"\u2192 The from address must match your verified domain",
|
|
111
|
+
"\u2192 Check your domain: `brokr status <stack>`"
|
|
112
|
+
].join("\n"),
|
|
113
|
+
STORAGE_UPLOAD_FAILED: [
|
|
114
|
+
"\u2192 File upload failed",
|
|
115
|
+
"\u2192 Check file size limits and retry"
|
|
116
|
+
].join("\n"),
|
|
117
|
+
STORAGE_NOT_FOUND: [
|
|
118
|
+
"\u2192 The requested file does not exist",
|
|
119
|
+
"\u2192 Check the key and try again"
|
|
120
|
+
].join("\n"),
|
|
121
|
+
PAYMENTS_NOT_CONFIGURED: [
|
|
122
|
+
"\u2192 Payments are not set up for this stack",
|
|
123
|
+
"\u2192 Run `brokr payments sync` to configure Stripe"
|
|
124
|
+
].join("\n"),
|
|
125
|
+
PAYMENTS_FAILED: [
|
|
126
|
+
"\u2192 Payment processing failed",
|
|
127
|
+
"\u2192 Check your Stripe dashboard for details"
|
|
128
|
+
].join("\n"),
|
|
129
|
+
GATEWAY_INTERNAL: [
|
|
130
|
+
"\u2192 The Brokr gateway encountered an internal error",
|
|
131
|
+
"\u2192 Check brokr.sh/status for service health"
|
|
132
|
+
].join("\n"),
|
|
133
|
+
UPSTREAM_ERROR: [
|
|
134
|
+
"\u2192 An upstream service is temporarily unavailable",
|
|
135
|
+
"\u2192 Retry in a few seconds \u2014 this is usually transient"
|
|
136
|
+
].join("\n"),
|
|
137
|
+
NETWORK_ERROR: [
|
|
138
|
+
"\u2192 Could not reach the Brokr gateway",
|
|
139
|
+
"\u2192 Check your internet connection",
|
|
140
|
+
"\u2192 Check brokr.sh/status for service health"
|
|
141
|
+
].join("\n"),
|
|
142
|
+
TIMEOUT: [
|
|
143
|
+
"\u2192 Request timed out",
|
|
144
|
+
"\u2192 Retry \u2014 if persistent, check brokr.sh/status"
|
|
145
|
+
].join("\n")
|
|
13
146
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
147
|
+
|
|
148
|
+
// src/errors.ts
|
|
149
|
+
var BrokrError = class extends Error {
|
|
150
|
+
constructor(message, code, capability, retryable = false, errorCode, requestId, hint) {
|
|
151
|
+
super(message);
|
|
152
|
+
this.code = code;
|
|
153
|
+
this.capability = capability;
|
|
154
|
+
this.retryable = retryable;
|
|
155
|
+
this.errorCode = errorCode;
|
|
156
|
+
this.requestId = requestId;
|
|
157
|
+
this.hint = hint;
|
|
158
|
+
this.name = "BrokrError";
|
|
159
|
+
}
|
|
160
|
+
/** Multi-line terminal fix block — looked up from error code. */
|
|
161
|
+
get fix() {
|
|
162
|
+
if (!this.errorCode) return void 0;
|
|
163
|
+
return FIX_REGISTRY[this.errorCode];
|
|
164
|
+
}
|
|
165
|
+
/** Documentation URL for this error code. */
|
|
166
|
+
get docsUrl() {
|
|
167
|
+
if (!this.errorCode) return void 0;
|
|
168
|
+
return `https://brokr.sh/errors/${this.errorCode.toLowerCase().replace(/_/g, "-")}`;
|
|
169
|
+
}
|
|
170
|
+
/** Safe message for end users — zero technical language. */
|
|
171
|
+
toUserMessage() {
|
|
172
|
+
switch (this.code) {
|
|
173
|
+
case "RATE_LIMITED":
|
|
174
|
+
return "Please wait a moment and try again.";
|
|
175
|
+
case "TIMEOUT":
|
|
176
|
+
return "The request took too long. Please try again.";
|
|
177
|
+
case "NETWORK_ERROR":
|
|
178
|
+
return "Connection issue. Check your internet and try again.";
|
|
179
|
+
case "BROKR_TOKEN_MISSING":
|
|
180
|
+
return "App is not connected to Brokr. Contact the developer.";
|
|
181
|
+
case "BROKR_TOKEN_INVALID":
|
|
182
|
+
return "Session expired. Please refresh.";
|
|
183
|
+
case "AI_BUDGET_EXCEEDED":
|
|
184
|
+
return "AI usage limit reached. The developer needs to add credits.";
|
|
185
|
+
case "AI_PROVIDER_UNAVAILABLE":
|
|
186
|
+
return "AI service is temporarily down. Please retry shortly.";
|
|
187
|
+
case "AI_PROVIDER_RATE_LIMITED":
|
|
188
|
+
return "Too many AI requests. Please wait a moment.";
|
|
189
|
+
case "INSUFFICIENT_CREDITS":
|
|
190
|
+
return "Not enough credits. Please top up your balance.";
|
|
191
|
+
case "STORAGE_UPLOAD_FAILED":
|
|
192
|
+
return "File upload failed. Please try again.";
|
|
193
|
+
case "STORAGE_NOT_FOUND":
|
|
194
|
+
return "File not found.";
|
|
195
|
+
case "EMAIL_SEND_FAILED":
|
|
196
|
+
return "Email could not be sent. Please try again.";
|
|
197
|
+
case "EMAIL_NOT_CONFIGURED":
|
|
198
|
+
return "Email is not set up for this app.";
|
|
199
|
+
case "PAYMENTS_NOT_CONFIGURED":
|
|
200
|
+
return "Payments are not configured. Contact the developer.";
|
|
201
|
+
case "PAYMENTS_FAILED":
|
|
202
|
+
return "Payment processing failed. Please try again.";
|
|
203
|
+
case "NOT_FOUND":
|
|
204
|
+
return "The requested resource was not found.";
|
|
205
|
+
case "VALIDATION_ERROR":
|
|
206
|
+
return "Invalid input. Please check your data and try again.";
|
|
207
|
+
default:
|
|
208
|
+
return "Something went wrong. We're looking into it.";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
toString() {
|
|
212
|
+
return `${this.name} [${this.errorCode ?? this.code}]: ${this.message}`;
|
|
213
|
+
}
|
|
214
|
+
toJSON() {
|
|
215
|
+
return {
|
|
216
|
+
name: this.name,
|
|
217
|
+
code: this.code,
|
|
218
|
+
errorCode: this.errorCode,
|
|
219
|
+
message: this.message,
|
|
220
|
+
capability: this.capability,
|
|
221
|
+
retryable: this.retryable,
|
|
222
|
+
requestId: this.requestId,
|
|
223
|
+
hint: this.hint,
|
|
224
|
+
component: this.component
|
|
225
|
+
};
|
|
226
|
+
}
|
|
17
227
|
};
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
228
|
+
var BrokrAuthError = class extends BrokrError {
|
|
229
|
+
constructor(message, code) {
|
|
230
|
+
super(message, code, "auth", false);
|
|
231
|
+
this.name = "BrokrAuthError";
|
|
23
232
|
}
|
|
24
|
-
return to;
|
|
25
233
|
};
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
BrokrAuthClient: () => BrokrAuthClient,
|
|
32
|
-
authMiddleware: () => authMiddleware
|
|
33
|
-
});
|
|
34
|
-
function parseCookies(cookieHeader) {
|
|
35
|
-
const cookies = /* @__PURE__ */ new Map();
|
|
36
|
-
for (const pair of cookieHeader.split(";")) {
|
|
37
|
-
const eqIdx = pair.indexOf("=");
|
|
38
|
-
if (eqIdx === -1) continue;
|
|
39
|
-
const name = pair.slice(0, eqIdx).trim();
|
|
40
|
-
const value = pair.slice(eqIdx + 1).trim();
|
|
41
|
-
if (name) cookies.set(name, value);
|
|
234
|
+
var BrokrRateLimitError = class extends BrokrError {
|
|
235
|
+
constructor(message, retryAfter, capability) {
|
|
236
|
+
super(message, "RATE_LIMITED", capability, true);
|
|
237
|
+
this.retryAfter = retryAfter;
|
|
238
|
+
this.name = "BrokrRateLimitError";
|
|
42
239
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const url = new URL(request.url);
|
|
49
|
-
const pathname = url.pathname;
|
|
50
|
-
const isProtected = protectedRoutes.some((route) => pathname.startsWith(route));
|
|
51
|
-
const isPublicOnly = publicOnlyRoutes.some((route) => pathname.startsWith(route));
|
|
52
|
-
if (!isProtected && !isPublicOnly) return void 0;
|
|
53
|
-
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
54
|
-
const cookies = parseCookies(cookieHeader);
|
|
55
|
-
const hasSession = cookies.has("better-auth.session_token");
|
|
56
|
-
if (isProtected && !hasSession) {
|
|
57
|
-
return Response.redirect(new URL("/sign-in", request.url).toString(), 302);
|
|
58
|
-
}
|
|
59
|
-
if (isPublicOnly && hasSession) {
|
|
60
|
-
return Response.redirect(new URL("/", request.url).toString(), 302);
|
|
61
|
-
}
|
|
62
|
-
return void 0;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
var BrokrAuthClient;
|
|
66
|
-
var init_auth = __esm({
|
|
67
|
-
"src/auth.ts"() {
|
|
68
|
-
"use strict";
|
|
69
|
-
init_runtime();
|
|
70
|
-
BrokrAuthClient = class {
|
|
71
|
-
constructor(token, gatewayUrl, appUrl) {
|
|
72
|
-
this._token = token;
|
|
73
|
-
this._gatewayUrl = gatewayUrl;
|
|
74
|
-
this._appUrl = appUrl ?? (typeof process !== "undefined" ? process.env.BETTER_AUTH_URL : void 0);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Get current user from an incoming request's cookies.
|
|
78
|
-
* Calls the local Better Auth API (runs inside your app).
|
|
79
|
-
*/
|
|
80
|
-
async currentUser(request) {
|
|
81
|
-
const session = await this.getSession(request);
|
|
82
|
-
return session?.user ?? null;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Get the full session (user + metadata) from an incoming request.
|
|
86
|
-
* Calls the local Better Auth API.
|
|
87
|
-
*/
|
|
88
|
-
async getSession(request) {
|
|
89
|
-
const appUrl = this._appUrl;
|
|
90
|
-
if (!appUrl) {
|
|
91
|
-
throw new BrokrError(
|
|
92
|
-
"[brokr] BETTER_AUTH_URL is not set. Auth may not be provisioned.",
|
|
93
|
-
"AUTH_NOT_CONFIGURED",
|
|
94
|
-
"auth"
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
98
|
-
if (!cookieHeader) return null;
|
|
99
|
-
const res = await fetch(`${appUrl}/api/auth/get-session`, {
|
|
100
|
-
method: "GET",
|
|
101
|
-
headers: { cookie: cookieHeader }
|
|
102
|
-
});
|
|
103
|
-
if (!res.ok) return null;
|
|
104
|
-
const data = await res.json();
|
|
105
|
-
if (!data?.user || !data?.session) return null;
|
|
106
|
-
return {
|
|
107
|
-
user: {
|
|
108
|
-
id: data.user.id,
|
|
109
|
-
email: data.user.email,
|
|
110
|
-
name: data.user.name ?? null,
|
|
111
|
-
avatarUrl: data.user.image ?? null,
|
|
112
|
-
emailVerified: data.user.emailVerified ? /* @__PURE__ */ new Date() : null
|
|
113
|
-
},
|
|
114
|
-
sessionId: data.session.id,
|
|
115
|
-
expiresAt: new Date(data.session.expiresAt)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Generate an OAuth authorization URL.
|
|
120
|
-
*/
|
|
121
|
-
async getOAuthUrl(provider, options) {
|
|
122
|
-
const appUrl = this._appUrl;
|
|
123
|
-
if (!appUrl) {
|
|
124
|
-
throw new BrokrError("[brokr] BETTER_AUTH_URL is not set.", "AUTH_NOT_CONFIGURED", "auth");
|
|
125
|
-
}
|
|
126
|
-
const redirectTo = options?.redirectTo ?? "/";
|
|
127
|
-
if (!redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
|
|
128
|
-
throw new BrokrError("[brokr] redirectTo must be a relative path (start with /)", "INVALID_REDIRECT", "auth");
|
|
129
|
-
}
|
|
130
|
-
const params = new URLSearchParams({ callbackURL: redirectTo });
|
|
131
|
-
return { redirectUrl: `${appUrl}/api/auth/sign-in/social?provider=${provider}&${params}` };
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Send a magic link email (requires email capability).
|
|
135
|
-
*/
|
|
136
|
-
async sendMagicLink(email, options) {
|
|
137
|
-
const appUrl = this._appUrl;
|
|
138
|
-
if (!appUrl) {
|
|
139
|
-
throw new BrokrError("[brokr] BETTER_AUTH_URL is not set.", "AUTH_NOT_CONFIGURED", "auth");
|
|
140
|
-
}
|
|
141
|
-
const res = await fetch(`${appUrl}/api/auth/magic-link/send`, {
|
|
142
|
-
method: "POST",
|
|
143
|
-
headers: { "Content-Type": "application/json" },
|
|
144
|
-
body: JSON.stringify({ email, callbackURL: options?.redirectTo ?? "/" })
|
|
145
|
-
});
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const data = await res.json().catch(() => ({}));
|
|
148
|
-
throw new BrokrError(
|
|
149
|
-
data.message ?? `[brokr] Failed to send magic link (HTTP ${res.status})`,
|
|
150
|
-
"AUTH_MAGIC_LINK_FAILED",
|
|
151
|
-
"auth"
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
};
|
|
240
|
+
};
|
|
241
|
+
var BrokrNetworkError = class extends BrokrError {
|
|
242
|
+
constructor(message, capability) {
|
|
243
|
+
super(message, "NETWORK_ERROR", capability, true);
|
|
244
|
+
this.name = "BrokrNetworkError";
|
|
156
245
|
}
|
|
157
|
-
}
|
|
246
|
+
};
|
|
247
|
+
var BrokrTimeoutError = class extends BrokrError {
|
|
248
|
+
constructor(message, capability) {
|
|
249
|
+
super(message, "TIMEOUT", capability, true);
|
|
250
|
+
this.name = "BrokrTimeoutError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var BrokrNotFoundError = class extends BrokrError {
|
|
254
|
+
constructor(message, capability) {
|
|
255
|
+
super(message, "NOT_FOUND", capability, false);
|
|
256
|
+
this.name = "BrokrNotFoundError";
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var BrokrValidationError = class extends BrokrError {
|
|
260
|
+
constructor(message, capability) {
|
|
261
|
+
super(message, "VALIDATION_ERROR", capability, false);
|
|
262
|
+
this.name = "BrokrValidationError";
|
|
263
|
+
}
|
|
264
|
+
};
|
|
158
265
|
|
|
159
|
-
// src/
|
|
266
|
+
// src/gateway.ts
|
|
267
|
+
var GATEWAY_URL = "https://api.brokr.sh";
|
|
268
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
160
269
|
function resolveToken() {
|
|
161
270
|
return typeof process !== "undefined" ? process.env.BROKR_TOKEN : void 0;
|
|
162
271
|
}
|
|
163
|
-
function
|
|
272
|
+
function requireToken(token, capability) {
|
|
164
273
|
if (!token) {
|
|
165
274
|
let hint = "brokr env pull --stack <name>";
|
|
166
275
|
try {
|
|
@@ -176,326 +285,1905 @@ function assertToken(token, capability) {
|
|
|
176
285
|
} catch {
|
|
177
286
|
}
|
|
178
287
|
throw new BrokrAuthError(
|
|
179
|
-
`
|
|
180
|
-
Run: ${hint}`,
|
|
288
|
+
`BROKR_TOKEN is not set. Run: ${hint}`,
|
|
181
289
|
"BROKR_TOKEN_MISSING"
|
|
182
290
|
);
|
|
183
291
|
}
|
|
184
292
|
}
|
|
185
293
|
async function gatewayFetch(gatewayUrl, token, path, body, capability) {
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
186
296
|
let res;
|
|
187
297
|
try {
|
|
188
298
|
res = await fetch(`${gatewayUrl}${path}`, {
|
|
189
299
|
method: "POST",
|
|
190
300
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
191
|
-
body: JSON.stringify(body)
|
|
301
|
+
body: JSON.stringify(body),
|
|
302
|
+
signal: controller.signal
|
|
192
303
|
});
|
|
193
304
|
} catch (err) {
|
|
305
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
306
|
+
throw new BrokrTimeoutError(
|
|
307
|
+
`Request timed out after ${FETCH_TIMEOUT_MS / 1e3}s`,
|
|
308
|
+
capability
|
|
309
|
+
);
|
|
310
|
+
}
|
|
194
311
|
throw new BrokrNetworkError(
|
|
195
|
-
|
|
196
|
-
${err instanceof Error ? err.message : String(err)}`,
|
|
312
|
+
"Could not reach Brokr gateway. Check your network.",
|
|
197
313
|
capability
|
|
198
314
|
);
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timeout);
|
|
199
317
|
}
|
|
200
318
|
if (res.status === 429) {
|
|
201
|
-
const
|
|
319
|
+
const rawRetryAfter = parseInt(res.headers.get("Retry-After") ?? "60", 10);
|
|
320
|
+
const retryAfter = Number.isFinite(rawRetryAfter) ? rawRetryAfter : 60;
|
|
202
321
|
const data = await res.json().catch(() => ({}));
|
|
322
|
+
const errorMsg = typeof data.error === "string" ? data.error : `Rate limited (retry after ${retryAfter}s)`;
|
|
203
323
|
throw new BrokrRateLimitError(
|
|
204
|
-
|
|
324
|
+
errorMsg,
|
|
205
325
|
retryAfter,
|
|
206
326
|
capability
|
|
207
327
|
);
|
|
208
328
|
}
|
|
209
329
|
if (res.status === 401) {
|
|
210
|
-
throw new BrokrAuthError("
|
|
330
|
+
throw new BrokrAuthError("Invalid or expired BROKR_TOKEN.", "BROKR_TOKEN_INVALID");
|
|
211
331
|
}
|
|
212
332
|
if (!res.ok) {
|
|
213
|
-
const
|
|
333
|
+
const body2 = await res.json().catch(() => ({}));
|
|
334
|
+
const errObj = typeof body2.error === "object" && body2.error !== null ? body2.error : void 0;
|
|
335
|
+
const errorData = body2.data ?? errObj?.data ?? body2;
|
|
336
|
+
const errorCode = errorData.errorCode ?? body2.code;
|
|
337
|
+
const hint = errorData.hint;
|
|
338
|
+
const requestId = errorData.requestId ?? res.headers.get("x-request-id");
|
|
339
|
+
const retryable = errorData.retryable;
|
|
340
|
+
const errorStr = typeof body2.error === "string" ? body2.error : void 0;
|
|
341
|
+
const message = body2.message ?? errObj?.message ?? errorStr ?? `${capability} request failed (HTTP ${res.status})`;
|
|
214
342
|
throw new BrokrError(
|
|
215
|
-
|
|
216
|
-
|
|
343
|
+
message,
|
|
344
|
+
errorCode ?? `${capability.toUpperCase()}_FAILED`,
|
|
345
|
+
capability,
|
|
346
|
+
retryable ?? false,
|
|
347
|
+
errorCode ?? void 0,
|
|
348
|
+
requestId ?? void 0,
|
|
349
|
+
hint ?? void 0
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
return await res.json();
|
|
354
|
+
} catch {
|
|
355
|
+
throw new BrokrError(
|
|
356
|
+
`${capability} returned invalid response (expected JSON).`,
|
|
357
|
+
`${capability.toUpperCase()}_INVALID_RESPONSE`,
|
|
358
|
+
capability,
|
|
359
|
+
true
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function gatewayStream(gatewayUrl, token, path, body, capability) {
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
366
|
+
let res;
|
|
367
|
+
try {
|
|
368
|
+
res = await fetch(`${gatewayUrl}${path}`, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
371
|
+
body: JSON.stringify(body),
|
|
372
|
+
signal: controller.signal
|
|
373
|
+
});
|
|
374
|
+
} catch (err) {
|
|
375
|
+
clearTimeout(timeout);
|
|
376
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
377
|
+
throw new BrokrTimeoutError(
|
|
378
|
+
`Stream request timed out after ${FETCH_TIMEOUT_MS / 1e3}s`,
|
|
379
|
+
capability
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
throw new BrokrNetworkError(
|
|
383
|
+
"Could not reach Brokr gateway. Check your network.",
|
|
217
384
|
capability
|
|
218
385
|
);
|
|
219
386
|
}
|
|
220
|
-
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
if (res.status === 429) {
|
|
389
|
+
const retryAfter = parseInt(res.headers.get("Retry-After") ?? "60", 10);
|
|
390
|
+
throw new BrokrRateLimitError("Rate limited.", retryAfter, capability);
|
|
391
|
+
}
|
|
392
|
+
if (res.status === 401) {
|
|
393
|
+
throw new BrokrAuthError("Invalid or expired BROKR_TOKEN.", "BROKR_TOKEN_INVALID");
|
|
394
|
+
}
|
|
395
|
+
if (!res.ok || !res.body) {
|
|
396
|
+
throw new BrokrError(
|
|
397
|
+
`${capability} stream failed (HTTP ${res.status})`,
|
|
398
|
+
`${capability.toUpperCase()}_STREAM_FAILED`,
|
|
399
|
+
capability
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return res;
|
|
221
403
|
}
|
|
222
|
-
|
|
223
|
-
|
|
404
|
+
|
|
405
|
+
// src/env-detect.ts
|
|
406
|
+
var cached = null;
|
|
407
|
+
function detectEnv() {
|
|
408
|
+
if (cached) return cached;
|
|
409
|
+
cached = _detect();
|
|
410
|
+
return cached;
|
|
224
411
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
412
|
+
function isDev() {
|
|
413
|
+
return detectEnv() === "development";
|
|
414
|
+
}
|
|
415
|
+
function isStaging() {
|
|
416
|
+
return detectEnv() === "staging";
|
|
417
|
+
}
|
|
418
|
+
function isProd() {
|
|
419
|
+
return detectEnv() === "production";
|
|
420
|
+
}
|
|
421
|
+
function _detect() {
|
|
422
|
+
if (typeof process !== "undefined" && process.env) {
|
|
423
|
+
const vercel = process.env.VERCEL;
|
|
424
|
+
const vercelEnv = process.env.VERCEL_ENV;
|
|
425
|
+
if (vercel === "1" || vercel === "true") {
|
|
426
|
+
if (vercelEnv === "preview") return "staging";
|
|
427
|
+
if (vercelEnv === "production") return "production";
|
|
428
|
+
return "production";
|
|
429
|
+
}
|
|
430
|
+
return "development";
|
|
431
|
+
}
|
|
432
|
+
if (typeof window !== "undefined" && window.location) {
|
|
433
|
+
const host = window.location.hostname;
|
|
434
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
|
|
435
|
+
return "development";
|
|
436
|
+
}
|
|
437
|
+
if (host.includes("-staging.brokr.sh") || host.includes(".preview.brokr.sh")) {
|
|
438
|
+
return "staging";
|
|
439
|
+
}
|
|
440
|
+
if (host.endsWith(".brokr.sh") || host === "brokr.sh") {
|
|
441
|
+
return "production";
|
|
442
|
+
}
|
|
443
|
+
return "production";
|
|
444
|
+
}
|
|
445
|
+
return "production";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/dev-console.ts
|
|
449
|
+
var BrokrDevConsole = class _BrokrDevConsole {
|
|
450
|
+
static {
|
|
451
|
+
this.installed = false;
|
|
452
|
+
}
|
|
453
|
+
static {
|
|
454
|
+
this.env = "production";
|
|
455
|
+
}
|
|
456
|
+
static install() {
|
|
457
|
+
if (_BrokrDevConsole.installed) return;
|
|
458
|
+
const env = detectEnv();
|
|
459
|
+
if (env === "production") return;
|
|
460
|
+
_BrokrDevConsole.installed = true;
|
|
461
|
+
_BrokrDevConsole.env = env;
|
|
462
|
+
if (typeof process !== "undefined" && process.on) {
|
|
463
|
+
process.on("unhandledRejection", (err) => {
|
|
464
|
+
if (err instanceof BrokrError) {
|
|
465
|
+
try {
|
|
466
|
+
_BrokrDevConsole.print(err);
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
static print(err) {
|
|
474
|
+
if (_BrokrDevConsole.env === "staging") {
|
|
475
|
+
const hint = err.hint ?? err.message;
|
|
476
|
+
console.error(`[brokr] ${err.errorCode ?? err.code}: ${hint}`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const lines = [
|
|
480
|
+
"",
|
|
481
|
+
"\u2501".repeat(41),
|
|
482
|
+
"",
|
|
483
|
+
` Brokr${err.component ? ` \u2014 ${err.component}` : ""}`,
|
|
484
|
+
"",
|
|
485
|
+
` ${err.message}`
|
|
486
|
+
];
|
|
487
|
+
if (err.fix) {
|
|
488
|
+
lines.push("", " Fix it:");
|
|
489
|
+
for (const line of err.fix.split("\n")) lines.push(` ${line}`);
|
|
490
|
+
}
|
|
491
|
+
if (err.docsUrl) {
|
|
492
|
+
lines.push("", ` Still broken? \u2192 ${err.docsUrl}`);
|
|
493
|
+
}
|
|
494
|
+
lines.push("", "\u2501".repeat(41), "");
|
|
495
|
+
console.error(lines.join("\n"));
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/logs/capture.ts
|
|
500
|
+
var DEFAULT_BATCH_SIZE = 20;
|
|
501
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
502
|
+
var buffer = [];
|
|
503
|
+
var flushTimer = null;
|
|
504
|
+
var config = null;
|
|
505
|
+
var intercepted = false;
|
|
506
|
+
function initCapture(opts = {}) {
|
|
507
|
+
if (config) return;
|
|
508
|
+
const token = opts.token ?? (typeof process !== "undefined" ? process.env.BROKR_TOKEN : void 0);
|
|
509
|
+
const stackId = opts.stackId ?? resolveStackId();
|
|
510
|
+
if (!token || !stackId) {
|
|
511
|
+
if (typeof process !== "undefined") {
|
|
512
|
+
console.log(`[brokr] Log capture skipped: token=${token ? "present" : "MISSING"} stackId=${stackId ?? "MISSING"}`);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const apiUrl = opts.apiUrl ?? opts.gatewayUrl ?? (typeof process !== "undefined" ? process.env.BROKR_GATEWAY_URL : void 0) ?? "https://api.brokr.sh";
|
|
517
|
+
config = {
|
|
518
|
+
token,
|
|
519
|
+
stackId,
|
|
520
|
+
apiUrl: apiUrl.replace(/\/+$/, ""),
|
|
521
|
+
batchSize: opts.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
522
|
+
flushIntervalMs: opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS
|
|
523
|
+
};
|
|
524
|
+
if (typeof process !== "undefined") {
|
|
525
|
+
console.log(`[brokr] Log capture initialized \u2192 ${apiUrl}/v1/logs/ingest (stack: ${stackId.slice(0, 8)}...)`);
|
|
526
|
+
}
|
|
527
|
+
if (flushTimer) clearInterval(flushTimer);
|
|
528
|
+
flushTimer = setInterval(() => flush(), config.flushIntervalMs);
|
|
529
|
+
if (typeof process !== "undefined" && process.on) {
|
|
530
|
+
process.on("beforeExit", () => flush());
|
|
531
|
+
}
|
|
532
|
+
if (typeof process !== "undefined" && !intercepted) {
|
|
533
|
+
intercepted = true;
|
|
534
|
+
const origError = console.error;
|
|
535
|
+
const origWarn = console.warn;
|
|
536
|
+
console.error = (...args) => {
|
|
537
|
+
origError.apply(console, args);
|
|
538
|
+
try {
|
|
539
|
+
const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
540
|
+
if (!msg.startsWith("[brokr]")) capture("error", msg, "console.error");
|
|
541
|
+
} catch {
|
|
249
542
|
}
|
|
250
543
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
544
|
+
console.warn = (...args) => {
|
|
545
|
+
origWarn.apply(console, args);
|
|
546
|
+
try {
|
|
547
|
+
const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
548
|
+
if (!msg.startsWith("[brokr]")) capture("warn", msg, "console.warn");
|
|
549
|
+
} catch {
|
|
255
550
|
}
|
|
256
551
|
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function capture(level, message, source, stackTrace) {
|
|
555
|
+
if (!config) return;
|
|
556
|
+
buffer.push({
|
|
557
|
+
level,
|
|
558
|
+
message: message.slice(0, 1e4),
|
|
559
|
+
source: source ?? "app",
|
|
560
|
+
stackTrace: stackTrace?.slice(0, 5e4),
|
|
561
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
562
|
+
});
|
|
563
|
+
if (buffer.length >= (config.batchSize ?? DEFAULT_BATCH_SIZE)) {
|
|
564
|
+
flush();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function flush() {
|
|
568
|
+
if (!config || buffer.length === 0) return;
|
|
569
|
+
const entries = buffer.splice(0);
|
|
570
|
+
const { token, stackId, apiUrl } = config;
|
|
571
|
+
fetch(`${apiUrl}/v1/logs/ingest`, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: {
|
|
574
|
+
"Content-Type": "application/json",
|
|
575
|
+
Authorization: `Bearer ${token}`
|
|
576
|
+
},
|
|
577
|
+
body: JSON.stringify({ stackId, entries })
|
|
578
|
+
}).catch((err) => {
|
|
579
|
+
if (typeof console !== "undefined") {
|
|
580
|
+
console.warn("[brokr] Log flush failed:", err instanceof Error ? err.message : err);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function resolveStackId() {
|
|
585
|
+
if (typeof process === "undefined") return void 0;
|
|
586
|
+
if (process.env.BROKR_STACK_ID) return process.env.BROKR_STACK_ID;
|
|
587
|
+
try {
|
|
588
|
+
const fs = __require("fs");
|
|
589
|
+
const path = __require("path");
|
|
590
|
+
const brokrFile = path.join(process.cwd(), ".brokr");
|
|
591
|
+
if (fs.existsSync(brokrFile)) {
|
|
592
|
+
const content = fs.readFileSync(brokrFile, "utf8");
|
|
593
|
+
const match = content.match(/BROKR_STACK_ID=(.+)/);
|
|
594
|
+
if (match) return match[1].trim();
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/ai/client.ts
|
|
602
|
+
function normalizeInput(input) {
|
|
603
|
+
if (typeof input === "string") {
|
|
604
|
+
return [{ role: "user", content: input }];
|
|
605
|
+
}
|
|
606
|
+
return input;
|
|
607
|
+
}
|
|
608
|
+
var BrokrAIClient = class {
|
|
609
|
+
constructor(_token, _gatewayUrl) {
|
|
610
|
+
this._token = _token;
|
|
611
|
+
this._gatewayUrl = _gatewayUrl;
|
|
612
|
+
}
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Core chat
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
/**
|
|
617
|
+
* Send a chat completion request.
|
|
618
|
+
* Accepts a string (auto-wrapped as user message) or a message array.
|
|
619
|
+
*/
|
|
620
|
+
async chat(input, options) {
|
|
621
|
+
requireToken(this._token, "ai");
|
|
622
|
+
const messages = normalizeInput(input);
|
|
623
|
+
try {
|
|
624
|
+
const data = await gatewayFetch(this._gatewayUrl, this._token, "/v1/chat/completions", {
|
|
625
|
+
messages,
|
|
626
|
+
model: options?.model,
|
|
627
|
+
max_tokens: options?.maxTokens,
|
|
628
|
+
temperature: options?.temperature
|
|
629
|
+
}, "ai");
|
|
630
|
+
return {
|
|
631
|
+
content: data.choices?.[0]?.message?.content ?? "",
|
|
632
|
+
model: data.model ?? "",
|
|
633
|
+
usage: {
|
|
634
|
+
promptTokens: data.usage?.prompt_tokens ?? 0,
|
|
635
|
+
completionTokens: data.usage?.completion_tokens ?? 0
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
} catch (err) {
|
|
639
|
+
if (err instanceof BrokrError) err.component = "AIChat";
|
|
640
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
641
|
+
capture("error", `AI chat failed: ${msg}`, "brokr.ai.chat", err instanceof Error ? err.stack : void 0);
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Stream a chat completion. Yields text strings directly.
|
|
647
|
+
* Accepts a string (auto-wrapped as user message) or a message array.
|
|
648
|
+
*/
|
|
649
|
+
/**
|
|
650
|
+
* Stream a chat completion. Yields text strings directly.
|
|
651
|
+
* Tries streaming first — if the gateway or provider doesn't support it,
|
|
652
|
+
* falls back to a non-streaming call and yields the full response at once.
|
|
653
|
+
*/
|
|
654
|
+
async *stream(input, options) {
|
|
655
|
+
requireToken(this._token, "ai");
|
|
656
|
+
const messages = normalizeInput(input);
|
|
657
|
+
let res;
|
|
658
|
+
try {
|
|
659
|
+
res = await gatewayStream(
|
|
660
|
+
this._gatewayUrl,
|
|
661
|
+
this._token,
|
|
662
|
+
"/v1/chat/completions",
|
|
663
|
+
{
|
|
284
664
|
messages,
|
|
665
|
+
stream: true,
|
|
285
666
|
model: options?.model,
|
|
286
667
|
max_tokens: options?.maxTokens,
|
|
287
668
|
temperature: options?.temperature
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
669
|
+
},
|
|
670
|
+
"ai"
|
|
671
|
+
);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
if (err instanceof BrokrError) err.component = "AIStream";
|
|
674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
capture("error", `AI stream failed: ${msg}`, "brokr.ai.stream", err instanceof Error ? err.stack : void 0);
|
|
676
|
+
try {
|
|
677
|
+
const fallback = await this.chat(input, options);
|
|
678
|
+
yield fallback.content;
|
|
679
|
+
} catch (fallbackErr) {
|
|
680
|
+
throw err;
|
|
297
681
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
685
|
+
if (!ct.includes("text/event-stream")) {
|
|
686
|
+
try {
|
|
687
|
+
const data = await res.json();
|
|
688
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
689
|
+
if (content) yield content;
|
|
690
|
+
} catch {
|
|
691
|
+
const fallback = await this.chat(input, options);
|
|
692
|
+
yield fallback.content;
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const reader = res.body.getReader();
|
|
697
|
+
const decoder = new TextDecoder();
|
|
698
|
+
let buffer2 = "";
|
|
699
|
+
while (true) {
|
|
700
|
+
const { done, value } = await reader.read();
|
|
701
|
+
if (done) break;
|
|
702
|
+
buffer2 += decoder.decode(value, { stream: true });
|
|
703
|
+
const lines = buffer2.split("\n");
|
|
704
|
+
buffer2 = lines.pop() ?? "";
|
|
705
|
+
for (const line of lines) {
|
|
706
|
+
if (!line.startsWith("data: ")) continue;
|
|
707
|
+
const payload = line.slice(6).trim();
|
|
708
|
+
if (payload === "[DONE]") return;
|
|
311
709
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
317
|
-
} catch (err) {
|
|
318
|
-
throw new BrokrNetworkError(
|
|
319
|
-
`[brokr] Could not reach Brokr gateway.
|
|
320
|
-
${err instanceof Error ? err.message : String(err)}`,
|
|
321
|
-
"ai"
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
if (res.status === 429) {
|
|
325
|
-
const retryAfter = parseInt(res.headers.get("Retry-After") ?? "60", 10);
|
|
326
|
-
throw new BrokrRateLimitError("[brokr] AI rate limited.", retryAfter, "ai");
|
|
327
|
-
}
|
|
328
|
-
if (res.status === 401) {
|
|
329
|
-
throw new BrokrAuthError("[brokr] Invalid or expired BROKR_TOKEN.", "BROKR_TOKEN_INVALID");
|
|
330
|
-
}
|
|
331
|
-
if (!res.ok || !res.body) {
|
|
332
|
-
throw new BrokrError(`[brokr] AI stream failed (HTTP ${res.status})`, "AI_STREAM_FAILED", "ai");
|
|
333
|
-
}
|
|
334
|
-
const reader = res.body.getReader();
|
|
335
|
-
const decoder = new TextDecoder();
|
|
336
|
-
let buffer = "";
|
|
337
|
-
while (true) {
|
|
338
|
-
const { done, value } = await reader.read();
|
|
339
|
-
if (done) break;
|
|
340
|
-
buffer += decoder.decode(value, { stream: true });
|
|
341
|
-
const lines = buffer.split("\n");
|
|
342
|
-
buffer = lines.pop() ?? "";
|
|
343
|
-
for (const line of lines) {
|
|
344
|
-
if (!line.startsWith("data: ")) continue;
|
|
345
|
-
const payload = line.slice(6).trim();
|
|
346
|
-
if (payload === "[DONE]") return;
|
|
347
|
-
try {
|
|
348
|
-
const parsed = JSON.parse(payload);
|
|
349
|
-
const delta = parsed.choices?.[0]?.delta?.content ?? "";
|
|
350
|
-
if (delta) yield delta;
|
|
351
|
-
} catch {
|
|
352
|
-
}
|
|
353
|
-
}
|
|
710
|
+
const parsed = JSON.parse(payload);
|
|
711
|
+
const delta = parsed.choices?.[0]?.delta?.content ?? "";
|
|
712
|
+
if (delta) yield delta;
|
|
713
|
+
} catch {
|
|
354
714
|
}
|
|
355
715
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
// Higher-level primitives
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
/**
|
|
722
|
+
* Extract structured data from a prompt using JSON mode.
|
|
723
|
+
* Returns a parsed object matching the provided schema shape.
|
|
724
|
+
*/
|
|
725
|
+
async structured(params) {
|
|
726
|
+
requireToken(this._token, "ai");
|
|
727
|
+
const schemaStr = JSON.stringify(params.schema, null, 2);
|
|
728
|
+
const messages = [
|
|
729
|
+
{
|
|
730
|
+
role: "system",
|
|
731
|
+
content: `You are a structured data extraction assistant. Return ONLY valid JSON matching this schema:
|
|
732
|
+
${schemaStr}
|
|
733
|
+
Do not include any other text, markdown, or explanation.`
|
|
734
|
+
},
|
|
735
|
+
{ role: "user", content: params.prompt }
|
|
736
|
+
];
|
|
737
|
+
try {
|
|
738
|
+
const data = await gatewayFetch(this._gatewayUrl, this._token, "/v1/chat/completions", {
|
|
739
|
+
messages,
|
|
740
|
+
model: params.model,
|
|
741
|
+
temperature: params.temperature ?? 0,
|
|
742
|
+
response_format: { type: "json_object" }
|
|
743
|
+
}, "ai");
|
|
744
|
+
const raw = data.choices?.[0]?.message?.content ?? "{}";
|
|
745
|
+
try {
|
|
746
|
+
return JSON.parse(raw);
|
|
747
|
+
} catch {
|
|
748
|
+
throw new BrokrError(
|
|
749
|
+
"[brokr] AI returned invalid JSON for structured extraction.",
|
|
750
|
+
"AI_STRUCTURED_PARSE_ERROR",
|
|
751
|
+
"ai"
|
|
752
|
+
);
|
|
366
753
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
754
|
+
} catch (err) {
|
|
755
|
+
if (err instanceof BrokrError) err.component = "AIStructured";
|
|
756
|
+
throw err;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Extract fields from unstructured text.
|
|
761
|
+
* Semantic alias for structured() — same behavior, clearer intent.
|
|
762
|
+
*/
|
|
763
|
+
async extract(prompt, schema) {
|
|
764
|
+
return this.structured({ prompt, schema });
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Summarize text using AI.
|
|
768
|
+
*/
|
|
769
|
+
async summarize(text, options) {
|
|
770
|
+
try {
|
|
771
|
+
const lengthHint = options?.maxLength ? ` Keep it under ${options.maxLength}.` : "";
|
|
772
|
+
const response = await this.chat(
|
|
773
|
+
[
|
|
774
|
+
{
|
|
775
|
+
role: "system",
|
|
776
|
+
content: `You are a summarization assistant. Provide a concise, accurate summary of the following text.${lengthHint} Return only the summary, no preamble.`
|
|
777
|
+
},
|
|
778
|
+
{ role: "user", content: text }
|
|
779
|
+
],
|
|
780
|
+
{ model: options?.model }
|
|
781
|
+
);
|
|
782
|
+
return { summary: response.content };
|
|
783
|
+
} catch (err) {
|
|
784
|
+
if (err instanceof BrokrError) err.component = "AISummarize";
|
|
785
|
+
throw err;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Classify text into one of the provided labels.
|
|
790
|
+
*/
|
|
791
|
+
async classify(text, labels, options) {
|
|
792
|
+
try {
|
|
793
|
+
const labelsStr = labels.map((l) => `"${l}"`).join(", ");
|
|
794
|
+
const result = await this.structured({
|
|
795
|
+
prompt: `Classify the following text into exactly one of these labels: [${labelsStr}]
|
|
796
|
+
|
|
797
|
+
Text: ${text}`,
|
|
798
|
+
schema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
label: { type: "string", enum: labels },
|
|
802
|
+
confidence: { type: "number", minimum: 0, maximum: 1 }
|
|
803
|
+
},
|
|
804
|
+
required: ["label", "confidence"]
|
|
805
|
+
},
|
|
806
|
+
model: options?.model,
|
|
807
|
+
temperature: 0
|
|
808
|
+
});
|
|
809
|
+
return result;
|
|
810
|
+
} catch (err) {
|
|
811
|
+
if (err instanceof BrokrError) err.component = "AIClassify";
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Generate an embedding vector for the given text.
|
|
817
|
+
*/
|
|
818
|
+
async embed(text) {
|
|
819
|
+
requireToken(this._token, "ai");
|
|
820
|
+
try {
|
|
821
|
+
const data = await gatewayFetch(this._gatewayUrl, this._token, "/v1/embeddings", {
|
|
822
|
+
input: text,
|
|
823
|
+
model: "text-embedding-3-small"
|
|
824
|
+
}, "ai");
|
|
825
|
+
return {
|
|
826
|
+
vector: data.data?.[0]?.embedding ?? []
|
|
827
|
+
};
|
|
828
|
+
} catch (err) {
|
|
829
|
+
if (err instanceof BrokrError) err.component = "AIEmbed";
|
|
830
|
+
throw err;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Generate an image from a text prompt.
|
|
835
|
+
*/
|
|
836
|
+
async image(prompt, options) {
|
|
837
|
+
requireToken(this._token, "ai");
|
|
838
|
+
try {
|
|
839
|
+
const data = await gatewayFetch(this._gatewayUrl, this._token, "/v1/images/generate", {
|
|
840
|
+
prompt,
|
|
841
|
+
size: options?.size ?? "1024x1024",
|
|
842
|
+
n: options?.n ?? 1,
|
|
843
|
+
model: options?.model
|
|
844
|
+
}, "ai");
|
|
845
|
+
const url = data.data?.[0]?.url;
|
|
846
|
+
if (!url) {
|
|
847
|
+
throw new BrokrError("[brokr] Image generation returned no URL.", "AI_IMAGE_FAILED", "ai");
|
|
371
848
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
849
|
+
return { url };
|
|
850
|
+
} catch (err) {
|
|
851
|
+
if (err instanceof BrokrError) err.component = "AIImage";
|
|
852
|
+
throw err;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
// OpenAI-SDK compatibility
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
/** OpenAI-SDK compatible base URL. */
|
|
859
|
+
get baseURL() {
|
|
860
|
+
return `${this._gatewayUrl}/v1`;
|
|
861
|
+
}
|
|
862
|
+
/** Use as `apiKey` with the official OpenAI SDK to route through Brokr's gateway. */
|
|
863
|
+
get apiKey() {
|
|
864
|
+
requireToken(this._token, "ai");
|
|
865
|
+
return this._token;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// src/storage/client.ts
|
|
870
|
+
var MULTIPART_THRESHOLD = 100 * 1024 * 1024;
|
|
871
|
+
var DEFAULT_PART_SIZE = 100 * 1024 * 1024;
|
|
872
|
+
var PART_UPLOAD_RETRIES = 3;
|
|
873
|
+
var BrokrStorageClient = class {
|
|
874
|
+
constructor(_token, _gatewayUrl) {
|
|
875
|
+
this._token = _token;
|
|
876
|
+
this._gatewayUrl = _gatewayUrl;
|
|
877
|
+
}
|
|
878
|
+
async upload(paramsOrData, filename, contentTypeArg) {
|
|
879
|
+
try {
|
|
880
|
+
let data;
|
|
881
|
+
let filePath;
|
|
882
|
+
let contentType;
|
|
883
|
+
if (typeof paramsOrData === "object" && paramsOrData !== null && "file" in paramsOrData) {
|
|
884
|
+
data = paramsOrData.file;
|
|
885
|
+
filePath = paramsOrData.path ?? `upload-${Date.now()}`;
|
|
886
|
+
contentType = paramsOrData.contentType ?? "application/octet-stream";
|
|
887
|
+
} else {
|
|
888
|
+
data = paramsOrData;
|
|
889
|
+
filePath = filename ?? `upload-${Date.now()}`;
|
|
890
|
+
contentType = contentTypeArg ?? "application/octet-stream";
|
|
377
891
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
* @example
|
|
382
|
-
* ```typescript
|
|
383
|
-
* const { url, key } = await brokr.storage.getUploadUrl('avatar.png', 'image/png');
|
|
384
|
-
* await fetch(url, { method: 'PUT', body: file });
|
|
385
|
-
* ```
|
|
386
|
-
*/
|
|
387
|
-
async getUploadUrl(filename, contentType = "application/octet-stream") {
|
|
388
|
-
assertToken(this._token, "storage");
|
|
389
|
-
return gatewayFetch(
|
|
390
|
-
this._gatewayUrl,
|
|
391
|
-
this._token,
|
|
392
|
-
"/v1/storage/sign-upload",
|
|
393
|
-
{ filename, contentType },
|
|
394
|
-
"storage"
|
|
395
|
-
);
|
|
892
|
+
const size = getUploadSize(data);
|
|
893
|
+
if (size !== void 0 && size > MULTIPART_THRESHOLD) {
|
|
894
|
+
return await this._uploadMultipart(data, filePath, contentType, size);
|
|
396
895
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
* const { key } = await brokr.storage.upload(fileBuffer, 'photo.jpg', 'image/jpeg');
|
|
403
|
-
* ```
|
|
404
|
-
*/
|
|
405
|
-
async upload(data, filename, contentType = "application/octet-stream") {
|
|
406
|
-
const { url, key } = await this.getUploadUrl(filename, contentType);
|
|
407
|
-
const putRes = await fetch(url, {
|
|
408
|
-
method: "PUT",
|
|
409
|
-
headers: { "Content-Type": contentType },
|
|
410
|
-
body: data
|
|
411
|
-
});
|
|
412
|
-
if (!putRes.ok) {
|
|
413
|
-
throw new BrokrError(`[brokr] Upload failed (HTTP ${putRes.status})`, "STORAGE_UPLOAD_FAILED", "storage");
|
|
414
|
-
}
|
|
415
|
-
return { key };
|
|
896
|
+
const { url, key } = await this.signUpload({ fileName: filePath, contentType });
|
|
897
|
+
const uploadHeaders = { "Content-Type": contentType };
|
|
898
|
+
if (typeof window === "undefined") {
|
|
899
|
+
const origin = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? process.env.APP_URL ?? process.env.BROKR_AUTH_URL ?? (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : void 0) ?? "http://localhost:3000";
|
|
900
|
+
uploadHeaders.Origin = origin;
|
|
416
901
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
async url(key, options) {
|
|
427
|
-
assertToken(this._token, "storage");
|
|
428
|
-
return gatewayFetch(
|
|
429
|
-
this._gatewayUrl,
|
|
430
|
-
this._token,
|
|
431
|
-
"/v1/storage/sign-download",
|
|
432
|
-
{ key, expiresIn: options?.expiresIn },
|
|
902
|
+
const putRes = await fetch(url, {
|
|
903
|
+
method: "PUT",
|
|
904
|
+
headers: uploadHeaders,
|
|
905
|
+
body: data
|
|
906
|
+
});
|
|
907
|
+
if (!putRes.ok) {
|
|
908
|
+
throw new BrokrError(
|
|
909
|
+
`[brokr] Upload failed (HTTP ${putRes.status})`,
|
|
910
|
+
"STORAGE_UPLOAD_FAILED",
|
|
433
911
|
"storage"
|
|
434
912
|
);
|
|
435
913
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
914
|
+
const gatewayBase = this._gatewayUrl.replace(/\/+$/, "");
|
|
915
|
+
const permanentUrl = `${gatewayBase}/v1/storage/file/${encodeURI(key)}`;
|
|
916
|
+
return {
|
|
917
|
+
key,
|
|
918
|
+
url: permanentUrl,
|
|
919
|
+
name: filePath,
|
|
920
|
+
size,
|
|
921
|
+
type: contentType
|
|
922
|
+
};
|
|
923
|
+
} catch (err) {
|
|
924
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get a presigned download URL for a stored object.
|
|
930
|
+
*/
|
|
931
|
+
async signedUrl(key, options) {
|
|
932
|
+
requireToken(this._token, "storage");
|
|
933
|
+
try {
|
|
934
|
+
return await gatewayFetch(
|
|
935
|
+
this._gatewayUrl,
|
|
936
|
+
this._token,
|
|
937
|
+
"/v1/storage/sign-download",
|
|
938
|
+
{ key, expiresIn: options?.expiresIn },
|
|
939
|
+
"storage"
|
|
940
|
+
);
|
|
941
|
+
} catch (err) {
|
|
942
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
943
|
+
throw err;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/** Low-level escape hatch. Most apps should call upload() instead. */
|
|
947
|
+
async signUpload(params) {
|
|
948
|
+
requireToken(this._token, "storage");
|
|
949
|
+
try {
|
|
950
|
+
return await gatewayFetch(
|
|
951
|
+
this._gatewayUrl,
|
|
952
|
+
this._token,
|
|
953
|
+
"/v1/storage/sign-upload",
|
|
954
|
+
{ filename: params.fileName, contentType: params.contentType ?? "application/octet-stream" },
|
|
955
|
+
"storage"
|
|
956
|
+
);
|
|
957
|
+
} catch (err) {
|
|
958
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
959
|
+
throw err;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
// Multipart upload — handles files > 100MB, up to 20GB
|
|
964
|
+
// ---------------------------------------------------------------------------
|
|
965
|
+
/**
|
|
966
|
+
* Initiate a multipart upload via the gateway.
|
|
967
|
+
*/
|
|
968
|
+
async _initiateMultipart(params) {
|
|
969
|
+
requireToken(this._token, "storage");
|
|
970
|
+
return gatewayFetch(
|
|
971
|
+
this._gatewayUrl,
|
|
972
|
+
this._token,
|
|
973
|
+
"/v1/storage/multipart/initiate",
|
|
974
|
+
{
|
|
975
|
+
filename: params.fileName,
|
|
976
|
+
contentType: params.contentType,
|
|
977
|
+
totalSize: params.totalSize,
|
|
978
|
+
partCount: params.partCount
|
|
979
|
+
},
|
|
980
|
+
"storage"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get a presigned URL for uploading a single part.
|
|
985
|
+
*/
|
|
986
|
+
async _signPartUpload(params) {
|
|
987
|
+
requireToken(this._token, "storage");
|
|
988
|
+
return gatewayFetch(
|
|
989
|
+
this._gatewayUrl,
|
|
990
|
+
this._token,
|
|
991
|
+
"/v1/storage/multipart/sign-part",
|
|
992
|
+
params,
|
|
993
|
+
"storage"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Complete a multipart upload with part ETags.
|
|
998
|
+
*/
|
|
999
|
+
async _completeMultipart(params) {
|
|
1000
|
+
requireToken(this._token, "storage");
|
|
1001
|
+
return gatewayFetch(
|
|
1002
|
+
this._gatewayUrl,
|
|
1003
|
+
this._token,
|
|
1004
|
+
"/v1/storage/multipart/complete",
|
|
1005
|
+
params,
|
|
1006
|
+
"storage"
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Abort a multipart upload, cleaning up uploaded parts.
|
|
1011
|
+
*/
|
|
1012
|
+
async _abortMultipart(params) {
|
|
1013
|
+
requireToken(this._token, "storage");
|
|
1014
|
+
await gatewayFetch(
|
|
1015
|
+
this._gatewayUrl,
|
|
1016
|
+
this._token,
|
|
1017
|
+
"/v1/storage/multipart/abort",
|
|
1018
|
+
params,
|
|
1019
|
+
"storage"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Upload a large file using multipart upload.
|
|
1024
|
+
* Splits the file into parts, uploads each with retry, then completes.
|
|
1025
|
+
*/
|
|
1026
|
+
async _uploadMultipart(data, filePath, contentType, totalSize) {
|
|
1027
|
+
const bytes = await toUint8Array(data);
|
|
1028
|
+
const partSize = Math.max(DEFAULT_PART_SIZE, Math.ceil(totalSize / MAX_PARTS_PER_UPLOAD));
|
|
1029
|
+
const partCount = Math.ceil(totalSize / partSize);
|
|
1030
|
+
const { uploadId, key } = await this._initiateMultipart({
|
|
1031
|
+
fileName: filePath,
|
|
1032
|
+
contentType,
|
|
1033
|
+
totalSize,
|
|
1034
|
+
partCount
|
|
1035
|
+
});
|
|
1036
|
+
try {
|
|
1037
|
+
const completedParts = [];
|
|
1038
|
+
for (let i = 0; i < partCount; i++) {
|
|
1039
|
+
const partNumber = i + 1;
|
|
1040
|
+
const start = i * partSize;
|
|
1041
|
+
const end = Math.min(start + partSize, totalSize);
|
|
1042
|
+
const partData = bytes.slice(start, end);
|
|
1043
|
+
const { url } = await this._signPartUpload({ key, uploadId, partNumber });
|
|
1044
|
+
let lastError;
|
|
1045
|
+
for (let attempt = 0; attempt < PART_UPLOAD_RETRIES; attempt++) {
|
|
1046
|
+
try {
|
|
1047
|
+
const putRes = await fetch(url, {
|
|
1048
|
+
method: "PUT",
|
|
1049
|
+
body: partData
|
|
1050
|
+
});
|
|
1051
|
+
if (!putRes.ok) {
|
|
1052
|
+
throw new Error(`Part ${partNumber} upload failed (HTTP ${putRes.status})`);
|
|
1053
|
+
}
|
|
1054
|
+
const etag = putRes.headers.get("ETag");
|
|
1055
|
+
if (!etag) {
|
|
1056
|
+
throw new Error(`Part ${partNumber} upload returned no ETag`);
|
|
1057
|
+
}
|
|
1058
|
+
completedParts.push({ partNumber, etag });
|
|
1059
|
+
lastError = void 0;
|
|
1060
|
+
break;
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (lastError) {
|
|
1066
|
+
throw new BrokrError(
|
|
1067
|
+
`[brokr] Multipart upload failed at part ${partNumber} after ${PART_UPLOAD_RETRIES} retries: ${lastError.message}`,
|
|
1068
|
+
"STORAGE_UPLOAD_FAILED",
|
|
1069
|
+
"storage"
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
439
1072
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1073
|
+
completedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
1074
|
+
await this._completeMultipart({ key, uploadId, parts: completedParts });
|
|
1075
|
+
const gatewayBase = this._gatewayUrl.replace(/\/+$/, "");
|
|
1076
|
+
const permanentUrl = `${gatewayBase}/v1/storage/file/${encodeURI(key)}`;
|
|
1077
|
+
return {
|
|
1078
|
+
key,
|
|
1079
|
+
url: permanentUrl,
|
|
1080
|
+
name: filePath,
|
|
1081
|
+
size: totalSize,
|
|
1082
|
+
type: contentType
|
|
1083
|
+
};
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
try {
|
|
1086
|
+
await this._abortMultipart({ key, uploadId });
|
|
1087
|
+
} catch {
|
|
445
1088
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1089
|
+
throw err;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* List objects by prefix with pagination.
|
|
1094
|
+
*/
|
|
1095
|
+
async list(params) {
|
|
1096
|
+
requireToken(this._token, "storage");
|
|
1097
|
+
try {
|
|
1098
|
+
return await gatewayFetch(
|
|
1099
|
+
this._gatewayUrl,
|
|
1100
|
+
this._token,
|
|
1101
|
+
"/v1/storage/list",
|
|
1102
|
+
{
|
|
1103
|
+
prefix: params?.prefix,
|
|
1104
|
+
maxKeys: params?.maxKeys,
|
|
1105
|
+
cursor: params?.cursor
|
|
1106
|
+
},
|
|
1107
|
+
"storage"
|
|
1108
|
+
);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1111
|
+
throw err;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Delete an object by key.
|
|
1116
|
+
*/
|
|
1117
|
+
async delete(key) {
|
|
1118
|
+
requireToken(this._token, "storage");
|
|
1119
|
+
try {
|
|
1120
|
+
await gatewayFetch(
|
|
1121
|
+
this._gatewayUrl,
|
|
1122
|
+
this._token,
|
|
1123
|
+
"/v1/storage/delete",
|
|
1124
|
+
{ key },
|
|
1125
|
+
"storage"
|
|
1126
|
+
);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1129
|
+
throw err;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Copy an object from one key to another.
|
|
1134
|
+
*/
|
|
1135
|
+
async copy(from, to) {
|
|
1136
|
+
requireToken(this._token, "storage");
|
|
1137
|
+
try {
|
|
1138
|
+
return await gatewayFetch(
|
|
1139
|
+
this._gatewayUrl,
|
|
1140
|
+
this._token,
|
|
1141
|
+
"/v1/storage/copy",
|
|
1142
|
+
{ from, to },
|
|
1143
|
+
"storage"
|
|
1144
|
+
);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1147
|
+
throw err;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Move an object (copy then delete source).
|
|
1152
|
+
*/
|
|
1153
|
+
async move(from, to) {
|
|
1154
|
+
try {
|
|
1155
|
+
const copied = await this.copy(from, to);
|
|
1156
|
+
await this.delete(from);
|
|
1157
|
+
return copied;
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1160
|
+
throw err;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if an object exists.
|
|
1165
|
+
*/
|
|
1166
|
+
/**
|
|
1167
|
+
* Check if an object exists.
|
|
1168
|
+
*
|
|
1169
|
+
* Returns `false` on any error (not-found, rate-limit, network).
|
|
1170
|
+
* This preserves backwards compatibility — `if (await brokr.storage.exists(key))`
|
|
1171
|
+
* must never throw in user code that doesn't wrap it in try-catch.
|
|
1172
|
+
*
|
|
1173
|
+
* If you need to distinguish "doesn't exist" from "couldn't check",
|
|
1174
|
+
* use `metadata()` instead and catch the error.
|
|
1175
|
+
*/
|
|
1176
|
+
async exists(key) {
|
|
1177
|
+
requireToken(this._token, "storage");
|
|
1178
|
+
try {
|
|
1179
|
+
const result = await gatewayFetch(
|
|
1180
|
+
this._gatewayUrl,
|
|
1181
|
+
this._token,
|
|
1182
|
+
"/v1/storage/exists",
|
|
1183
|
+
{ key },
|
|
1184
|
+
"storage"
|
|
1185
|
+
);
|
|
1186
|
+
return result.exists;
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get file metadata without downloading the file.
|
|
1194
|
+
*/
|
|
1195
|
+
async metadata(key) {
|
|
1196
|
+
requireToken(this._token, "storage");
|
|
1197
|
+
try {
|
|
1198
|
+
return await gatewayFetch(
|
|
1199
|
+
this._gatewayUrl,
|
|
1200
|
+
this._token,
|
|
1201
|
+
"/v1/storage/metadata",
|
|
1202
|
+
{ key },
|
|
1203
|
+
"storage"
|
|
1204
|
+
);
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
if (err instanceof BrokrError) err.component = "Storage";
|
|
1207
|
+
throw err;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// ---------------------------------------------------------------------------
|
|
1211
|
+
// Deprecated aliases (backward compat)
|
|
1212
|
+
// ---------------------------------------------------------------------------
|
|
1213
|
+
/** @deprecated Use signedUrl() instead. */
|
|
1214
|
+
async url(key, options) {
|
|
1215
|
+
return this.signedUrl(key, options);
|
|
1216
|
+
}
|
|
1217
|
+
/** @deprecated Use signedUrl() instead. */
|
|
1218
|
+
async getUrl(key, options) {
|
|
1219
|
+
return this.signedUrl(key, options);
|
|
1220
|
+
}
|
|
1221
|
+
/** @deprecated Use signUpload() instead. */
|
|
1222
|
+
async getUploadUrl(filename, contentType) {
|
|
1223
|
+
return this.signUpload({ fileName: filename, contentType });
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
var MAX_PARTS_PER_UPLOAD = 1e4;
|
|
1227
|
+
async function toUint8Array(data) {
|
|
1228
|
+
if (data instanceof Uint8Array) return data;
|
|
1229
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
1230
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1231
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
1232
|
+
}
|
|
1233
|
+
throw new Error("Unsupported data type for multipart upload");
|
|
1234
|
+
}
|
|
1235
|
+
function getUploadSize(data) {
|
|
1236
|
+
if (typeof data === "string") {
|
|
1237
|
+
return new TextEncoder().encode(data).length;
|
|
1238
|
+
}
|
|
1239
|
+
if (data instanceof Uint8Array) {
|
|
1240
|
+
return data.byteLength;
|
|
1241
|
+
}
|
|
1242
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
1243
|
+
return data.size;
|
|
1244
|
+
}
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/email/templates.ts
|
|
1249
|
+
function interpolate(template, vars) {
|
|
1250
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
1251
|
+
}
|
|
1252
|
+
function wrapHtml(body) {
|
|
1253
|
+
return `<!DOCTYPE html>
|
|
1254
|
+
<html>
|
|
1255
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
1256
|
+
<body style="margin:0;padding:0;background:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
1257
|
+
<div style="max-width:560px;margin:40px auto;background:#ffffff;border-radius:8px;border:1px solid #e5e5e5;overflow:hidden;">
|
|
1258
|
+
${body}
|
|
1259
|
+
</div>
|
|
1260
|
+
<div style="text-align:center;padding:20px;color:#999;font-size:12px;">
|
|
1261
|
+
Sent via Brokr
|
|
1262
|
+
</div>
|
|
1263
|
+
</body>
|
|
1264
|
+
</html>`;
|
|
1265
|
+
}
|
|
1266
|
+
function section(content) {
|
|
1267
|
+
return `<div style="padding:32px 40px;">${content}</div>`;
|
|
1268
|
+
}
|
|
1269
|
+
function button(text, urlVar) {
|
|
1270
|
+
return `<a href="{{${urlVar}}}" style="display:inline-block;padding:12px 24px;background:#000;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;">${text}</a>`;
|
|
1271
|
+
}
|
|
1272
|
+
var welcomeTemplate = {
|
|
1273
|
+
name: "welcome",
|
|
1274
|
+
subject: "Welcome to {{appName}}",
|
|
1275
|
+
html: (vars) => interpolate(wrapHtml(section(`
|
|
1276
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#111;">Welcome to {{appName}}</h1>
|
|
1277
|
+
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">
|
|
1278
|
+
Your account is ready. You can start using {{appName}} right away.
|
|
1279
|
+
</p>
|
|
1280
|
+
${button("Get Started", "actionUrl")}
|
|
1281
|
+
`)), vars)
|
|
1282
|
+
};
|
|
1283
|
+
var magicLinkTemplate = {
|
|
1284
|
+
name: "magicLink",
|
|
1285
|
+
subject: "Your sign-in link",
|
|
1286
|
+
html: (vars) => interpolate(wrapHtml(section(`
|
|
1287
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#111;">Sign in to {{appName}}</h1>
|
|
1288
|
+
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">
|
|
1289
|
+
Click the button below to sign in. This link expires in 10 minutes.
|
|
1290
|
+
</p>
|
|
1291
|
+
${button("Sign In", "magicLinkUrl")}
|
|
1292
|
+
<p style="margin:24px 0 0;color:#999;font-size:13px;">
|
|
1293
|
+
If you didn't request this, you can safely ignore this email.
|
|
1294
|
+
</p>
|
|
1295
|
+
`)), vars)
|
|
1296
|
+
};
|
|
1297
|
+
var passwordResetTemplate = {
|
|
1298
|
+
name: "passwordReset",
|
|
1299
|
+
subject: "Reset your password",
|
|
1300
|
+
html: (vars) => interpolate(wrapHtml(section(`
|
|
1301
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#111;">Reset your password</h1>
|
|
1302
|
+
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">
|
|
1303
|
+
We received a request to reset your password for {{appName}}.
|
|
1304
|
+
Click the button below to choose a new password.
|
|
1305
|
+
</p>
|
|
1306
|
+
${button("Reset Password", "resetUrl")}
|
|
1307
|
+
<p style="margin:24px 0 0;color:#999;font-size:13px;">
|
|
1308
|
+
This link expires in 1 hour. If you didn't request this, ignore this email.
|
|
1309
|
+
</p>
|
|
1310
|
+
`)), vars)
|
|
1311
|
+
};
|
|
1312
|
+
var invoiceTemplate = {
|
|
1313
|
+
name: "invoice",
|
|
1314
|
+
subject: "Invoice #{{invoiceNumber}}",
|
|
1315
|
+
html: (vars) => interpolate(wrapHtml(section(`
|
|
1316
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#111;">Invoice #{{invoiceNumber}}</h1>
|
|
1317
|
+
<p style="margin:0 0 8px;color:#555;font-size:15px;line-height:1.6;">
|
|
1318
|
+
Amount: <strong>{{amount}}</strong>
|
|
1319
|
+
</p>
|
|
1320
|
+
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">
|
|
1321
|
+
{{description}}
|
|
1322
|
+
</p>
|
|
1323
|
+
${button("View Invoice", "invoiceUrl")}
|
|
1324
|
+
`)), vars)
|
|
1325
|
+
};
|
|
1326
|
+
var notificationTemplate = {
|
|
1327
|
+
name: "notification",
|
|
1328
|
+
subject: "{{title}}",
|
|
1329
|
+
html: (vars) => interpolate(wrapHtml(section(`
|
|
1330
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#111;">{{title}}</h1>
|
|
1331
|
+
<p style="margin:0 0 24px;color:#555;font-size:15px;line-height:1.6;">
|
|
1332
|
+
{{body}}
|
|
1333
|
+
</p>
|
|
1334
|
+
`)), vars)
|
|
1335
|
+
};
|
|
1336
|
+
var builtinTemplates = {
|
|
1337
|
+
welcome: welcomeTemplate,
|
|
1338
|
+
magicLink: magicLinkTemplate,
|
|
1339
|
+
passwordReset: passwordResetTemplate,
|
|
1340
|
+
invoice: invoiceTemplate,
|
|
1341
|
+
notification: notificationTemplate
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/email/client.ts
|
|
1345
|
+
function interpolate2(template, vars) {
|
|
1346
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
1347
|
+
}
|
|
1348
|
+
var BrokrEmailClient = class {
|
|
1349
|
+
constructor(_token, _gatewayUrl) {
|
|
1350
|
+
this._token = _token;
|
|
1351
|
+
this._gatewayUrl = _gatewayUrl;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Send an email. The from address and API credentials are resolved server-side.
|
|
1355
|
+
*/
|
|
1356
|
+
async send(params) {
|
|
1357
|
+
requireToken(this._token, "email");
|
|
1358
|
+
try {
|
|
1359
|
+
return await gatewayFetch(
|
|
1360
|
+
this._gatewayUrl,
|
|
1361
|
+
this._token,
|
|
1362
|
+
"/v1/email/send",
|
|
1363
|
+
params,
|
|
1364
|
+
"email"
|
|
1365
|
+
);
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
if (err instanceof BrokrError) err.component = "Email";
|
|
1368
|
+
throw err;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Send an email using a built-in template.
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* ```typescript
|
|
1376
|
+
* await brokr.email.sendTemplate({
|
|
1377
|
+
* template: 'welcome',
|
|
1378
|
+
* to: 'user@example.com',
|
|
1379
|
+
* variables: { appName: 'MyApp', actionUrl: 'https://myapp.com/dashboard' },
|
|
1380
|
+
* });
|
|
1381
|
+
* ```
|
|
1382
|
+
*/
|
|
1383
|
+
async sendTemplate(params) {
|
|
1384
|
+
try {
|
|
1385
|
+
const template = builtinTemplates[params.template];
|
|
1386
|
+
if (!template) {
|
|
1387
|
+
throw new BrokrError(
|
|
1388
|
+
`[brokr] Unknown email template "${params.template}". Available: ${Object.keys(builtinTemplates).join(", ")}`,
|
|
1389
|
+
"EMAIL_TEMPLATE_NOT_FOUND",
|
|
465
1390
|
"email"
|
|
466
1391
|
);
|
|
467
1392
|
}
|
|
1393
|
+
const subject = interpolate2(template.subject, params.variables);
|
|
1394
|
+
const html = template.html(params.variables);
|
|
1395
|
+
return await this.send({
|
|
1396
|
+
to: params.to,
|
|
1397
|
+
subject,
|
|
1398
|
+
html,
|
|
1399
|
+
from: params.from
|
|
1400
|
+
});
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
if (err instanceof BrokrError) err.component = "Email";
|
|
1403
|
+
throw err;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/files/client.ts
|
|
1409
|
+
var BrokrFilesClient = class {
|
|
1410
|
+
constructor(_token, _gatewayUrl, _storage) {
|
|
1411
|
+
this._token = _token;
|
|
1412
|
+
this._gatewayUrl = _gatewayUrl;
|
|
1413
|
+
this._storage = _storage;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Upload a file and trigger server-side AI processing.
|
|
1417
|
+
* Returns the storage key plus any extracted description/text.
|
|
1418
|
+
*/
|
|
1419
|
+
async process(params) {
|
|
1420
|
+
requireToken(this._token, "files");
|
|
1421
|
+
const uploaded = await this._storage.upload({
|
|
1422
|
+
file: params.file,
|
|
1423
|
+
path: params.fileName,
|
|
1424
|
+
contentType: params.purpose === "text-extraction" ? "application/pdf" : void 0
|
|
1425
|
+
});
|
|
1426
|
+
const result = await gatewayFetch(
|
|
1427
|
+
this._gatewayUrl,
|
|
1428
|
+
this._token,
|
|
1429
|
+
"/v1/files/process",
|
|
1430
|
+
{ key: uploaded.key, purpose: params.purpose ?? "general" },
|
|
1431
|
+
"files"
|
|
1432
|
+
);
|
|
1433
|
+
return {
|
|
1434
|
+
key: uploaded.key,
|
|
1435
|
+
description: result.description,
|
|
1436
|
+
text: result.text
|
|
468
1437
|
};
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Get an AI-generated description of an already-uploaded file.
|
|
1441
|
+
*/
|
|
1442
|
+
async describe(key) {
|
|
1443
|
+
requireToken(this._token, "files");
|
|
1444
|
+
return gatewayFetch(
|
|
1445
|
+
this._gatewayUrl,
|
|
1446
|
+
this._token,
|
|
1447
|
+
"/v1/files/describe",
|
|
1448
|
+
{ key },
|
|
1449
|
+
"files"
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Extract text from a PDF or image (OCR).
|
|
1454
|
+
*/
|
|
1455
|
+
async getText(key) {
|
|
1456
|
+
requireToken(this._token, "files");
|
|
1457
|
+
return gatewayFetch(
|
|
1458
|
+
this._gatewayUrl,
|
|
1459
|
+
this._token,
|
|
1460
|
+
"/v1/files/text",
|
|
1461
|
+
{ key },
|
|
1462
|
+
"files"
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/payments/client.ts
|
|
1468
|
+
var BrokrPaymentsClient = class {
|
|
1469
|
+
constructor(_token, _gatewayUrl) {
|
|
1470
|
+
this._token = _token;
|
|
1471
|
+
this._gatewayUrl = _gatewayUrl;
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Create a Stripe Checkout session for a plan.
|
|
1475
|
+
* Returns a URL to redirect the end user to.
|
|
1476
|
+
*
|
|
1477
|
+
* @example
|
|
1478
|
+
* ```ts
|
|
1479
|
+
* const user = await brokr.auth.requireUser(request.headers);
|
|
1480
|
+
* const { checkoutUrl } = await brokr.payments.checkout({
|
|
1481
|
+
* plan: 'pro',
|
|
1482
|
+
* userId: user.id,
|
|
1483
|
+
* });
|
|
1484
|
+
* // Redirect user to checkoutUrl
|
|
1485
|
+
* ```
|
|
1486
|
+
*/
|
|
1487
|
+
/**
|
|
1488
|
+
* Create a Stripe Checkout session for a plan.
|
|
1489
|
+
*
|
|
1490
|
+
* @param params.plan - Plan slug (e.g. 'pro')
|
|
1491
|
+
* @param params.userId - End user's ID from your auth system
|
|
1492
|
+
* @param params.returnUrl - Where to redirect after checkout. Defaults to your stack's URL.
|
|
1493
|
+
* Pass explicitly for localhost or non-Brokr deployments.
|
|
1494
|
+
*/
|
|
1495
|
+
async checkout(params) {
|
|
1496
|
+
requireToken(this._token, "payments");
|
|
1497
|
+
try {
|
|
1498
|
+
return await gatewayFetch(
|
|
1499
|
+
this._gatewayUrl,
|
|
1500
|
+
this._token,
|
|
1501
|
+
"/v1/payments/checkout",
|
|
1502
|
+
{ planSlug: params.plan, appUserId: params.userId, returnUrl: params.returnUrl },
|
|
1503
|
+
"payments"
|
|
1504
|
+
);
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
if (err instanceof BrokrError) err.component = "Payments";
|
|
1507
|
+
throw err;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Create a Stripe Customer Portal session.
|
|
1512
|
+
* Returns a URL where the end user can manage their subscription.
|
|
1513
|
+
*/
|
|
1514
|
+
/**
|
|
1515
|
+
* Create a Stripe Customer Portal session.
|
|
1516
|
+
*
|
|
1517
|
+
* @param params.userId - End user's ID
|
|
1518
|
+
* @param params.returnUrl - Where to redirect when they're done. Defaults to your stack's URL.
|
|
1519
|
+
*/
|
|
1520
|
+
async portal(params) {
|
|
1521
|
+
requireToken(this._token, "payments");
|
|
1522
|
+
try {
|
|
1523
|
+
return await gatewayFetch(
|
|
1524
|
+
this._gatewayUrl,
|
|
1525
|
+
this._token,
|
|
1526
|
+
"/v1/payments/portal",
|
|
1527
|
+
{ appUserId: params.userId, returnUrl: params.returnUrl },
|
|
1528
|
+
"payments"
|
|
1529
|
+
);
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
if (err instanceof BrokrError) err.component = "Payments";
|
|
1532
|
+
throw err;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Get the end user's current plan.
|
|
1537
|
+
* Returns null if the user has no subscription.
|
|
1538
|
+
*/
|
|
1539
|
+
async currentPlan(params) {
|
|
1540
|
+
requireToken(this._token, "payments");
|
|
1541
|
+
try {
|
|
1542
|
+
return await gatewayFetch(
|
|
1543
|
+
this._gatewayUrl,
|
|
1544
|
+
this._token,
|
|
1545
|
+
"/v1/payments/plan",
|
|
1546
|
+
{ appUserId: params.userId },
|
|
1547
|
+
"payments"
|
|
1548
|
+
);
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
if (err instanceof BrokrError) err.component = "Payments";
|
|
1551
|
+
throw err;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// src/payments/entitlements.ts
|
|
1557
|
+
var BrokrEntitlementsClient = class {
|
|
1558
|
+
constructor(_token, _gatewayUrl) {
|
|
1559
|
+
this._token = _token;
|
|
1560
|
+
this._gatewayUrl = _gatewayUrl;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Check if an end user is entitled to a feature.
|
|
1564
|
+
* Returns true/false without throwing.
|
|
1565
|
+
*
|
|
1566
|
+
* @example
|
|
1567
|
+
* ```ts
|
|
1568
|
+
* const user = await brokr.auth.requireUser(request.headers);
|
|
1569
|
+
* const canChat = await brokr.entitlements.check({ feature: 'ai.chat', userId: user.id });
|
|
1570
|
+
* ```
|
|
1571
|
+
*/
|
|
1572
|
+
async check(params) {
|
|
1573
|
+
requireToken(this._token, "payments");
|
|
1574
|
+
const result = await gatewayFetch(
|
|
1575
|
+
this._gatewayUrl,
|
|
1576
|
+
this._token,
|
|
1577
|
+
"/v1/payments/entitlements/check",
|
|
1578
|
+
{ feature: params.feature, appUserId: params.userId },
|
|
1579
|
+
"payments"
|
|
1580
|
+
);
|
|
1581
|
+
return result.allowed;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Require that an end user is entitled to a feature.
|
|
1585
|
+
* Throws BrokrError if not entitled.
|
|
1586
|
+
*/
|
|
1587
|
+
async require(params) {
|
|
1588
|
+
const allowed = await this.check(params);
|
|
1589
|
+
if (!allowed) {
|
|
1590
|
+
throw new BrokrError(
|
|
1591
|
+
`[brokr] Feature "${params.feature}" is not available on the user's current plan. Upgrade at the billing portal.`,
|
|
1592
|
+
"ENTITLEMENT_DENIED",
|
|
1593
|
+
"payments"
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Get usage stats for a metered feature.
|
|
1599
|
+
* This is a READ — does NOT increment the counter.
|
|
1600
|
+
* Use `increment()` to record usage.
|
|
1601
|
+
*/
|
|
1602
|
+
async usage(params) {
|
|
1603
|
+
requireToken(this._token, "payments");
|
|
1604
|
+
return gatewayFetch(
|
|
1605
|
+
this._gatewayUrl,
|
|
1606
|
+
this._token,
|
|
1607
|
+
"/v1/payments/entitlements/usage",
|
|
1608
|
+
{ feature: params.feature, appUserId: params.userId },
|
|
1609
|
+
"payments"
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Record a usage event for a metered feature.
|
|
1614
|
+
* This is a WRITE — increments the counter.
|
|
1615
|
+
*/
|
|
1616
|
+
async increment(params) {
|
|
1617
|
+
requireToken(this._token, "payments");
|
|
1618
|
+
await gatewayFetch(
|
|
1619
|
+
this._gatewayUrl,
|
|
1620
|
+
this._token,
|
|
1621
|
+
"/v1/payments/entitlements/increment",
|
|
1622
|
+
{ feature: params.feature, appUserId: params.userId, amount: params.amount ?? 1 },
|
|
1623
|
+
"payments"
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// src/notifications/client.ts
|
|
1629
|
+
var BrokrNotificationsClient = class {
|
|
1630
|
+
constructor(token, gatewayUrl) {
|
|
1631
|
+
this.token = token;
|
|
1632
|
+
this.gatewayUrl = gatewayUrl;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Send a notification to a user. Delivered via WebSocket in real-time
|
|
1636
|
+
* and persisted in the notification history.
|
|
1637
|
+
*/
|
|
1638
|
+
async send(params) {
|
|
1639
|
+
requireToken(this.token, "notifications");
|
|
1640
|
+
try {
|
|
1641
|
+
return await gatewayFetch(
|
|
1642
|
+
this.gatewayUrl,
|
|
1643
|
+
this.token,
|
|
1644
|
+
"/v1/notifications/push",
|
|
1645
|
+
params,
|
|
1646
|
+
"notifications"
|
|
1647
|
+
);
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
if (err instanceof BrokrError) err.component = "Notifications";
|
|
1650
|
+
throw err;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Fetch notification history for a user. Reads from the DO's SQLite
|
|
1655
|
+
* storage via a gateway REST endpoint.
|
|
1656
|
+
*/
|
|
1657
|
+
async list(params) {
|
|
1658
|
+
requireToken(this.token, "notifications");
|
|
1659
|
+
try {
|
|
1660
|
+
return await gatewayFetch(
|
|
1661
|
+
this.gatewayUrl,
|
|
1662
|
+
this.token,
|
|
1663
|
+
"/v1/notifications/list",
|
|
1664
|
+
params,
|
|
1665
|
+
"notifications"
|
|
1666
|
+
);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
if (err instanceof BrokrError) err.component = "Notifications";
|
|
1669
|
+
throw err;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
// src/auth.ts
|
|
1675
|
+
function resolveAppUrl(appUrl) {
|
|
1676
|
+
if (appUrl) return appUrl;
|
|
1677
|
+
throw new BrokrError(
|
|
1678
|
+
"[brokr] BROKR_AUTH_URL is not set. Auth may not be provisioned.",
|
|
1679
|
+
"AUTH_NOT_CONFIGURED",
|
|
1680
|
+
"auth"
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
function mapSessionUser(raw) {
|
|
1684
|
+
return {
|
|
1685
|
+
id: raw.id,
|
|
1686
|
+
email: raw.email,
|
|
1687
|
+
name: raw.name ?? null,
|
|
1688
|
+
image: raw.image ?? null,
|
|
1689
|
+
emailVerified: raw.emailVerified ? /* @__PURE__ */ new Date() : null
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function pluralizeResource(resource) {
|
|
1693
|
+
if (resource.endsWith("s")) return resource;
|
|
1694
|
+
if (resource.endsWith("y") && !/[aeiou]y$/i.test(resource)) return resource.slice(0, -1) + "ies";
|
|
1695
|
+
return resource + "s";
|
|
1696
|
+
}
|
|
1697
|
+
function validateSqlIdentifier(name, label) {
|
|
1698
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
1699
|
+
throw new BrokrError(
|
|
1700
|
+
`[brokr] Invalid ${label}: "${name}". Must be alphanumeric with underscores only.`,
|
|
1701
|
+
"INVALID_IDENTIFIER",
|
|
1702
|
+
"auth"
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
var _ownershipPool = null;
|
|
1707
|
+
async function getOwnershipPool(dbUrl) {
|
|
1708
|
+
let Pool;
|
|
1709
|
+
try {
|
|
1710
|
+
const mod = await import("@neondatabase/serverless");
|
|
1711
|
+
Pool = mod.Pool;
|
|
1712
|
+
} catch {
|
|
1713
|
+
throw new BrokrError(
|
|
1714
|
+
"[brokr] @neondatabase/serverless is required for ownership checks. Run: npm install @neondatabase/serverless",
|
|
1715
|
+
"MISSING_DEPENDENCY",
|
|
1716
|
+
"auth"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
if (_ownershipPool && _ownershipPool.dbUrl === dbUrl) {
|
|
1720
|
+
return _ownershipPool.pool;
|
|
1721
|
+
}
|
|
1722
|
+
const pool = new Pool({ connectionString: dbUrl, max: 3 });
|
|
1723
|
+
_ownershipPool = { pool, dbUrl };
|
|
1724
|
+
return pool;
|
|
1725
|
+
}
|
|
1726
|
+
async function authApiFetch(appUrl, path, options) {
|
|
1727
|
+
const headers = { "Content-Type": "application/json" };
|
|
1728
|
+
if (options?.cookies) headers.cookie = options.cookies;
|
|
1729
|
+
if (typeof window === "undefined") {
|
|
1730
|
+
headers.Origin = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? process.env.APP_URL ?? process.env.BROKR_AUTH_URL ?? (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : void 0) ?? appUrl;
|
|
1731
|
+
}
|
|
1732
|
+
const res = await fetch(`${appUrl}${path}`, {
|
|
1733
|
+
method: options?.method ?? "GET",
|
|
1734
|
+
headers,
|
|
1735
|
+
body: options?.body ? JSON.stringify(options.body) : void 0
|
|
1736
|
+
});
|
|
1737
|
+
if (!res.ok) {
|
|
1738
|
+
const data = await res.json().catch(() => ({}));
|
|
1739
|
+
throw new BrokrError(
|
|
1740
|
+
data.message ?? `[brokr] Auth API call failed (HTTP ${res.status})`,
|
|
1741
|
+
"AUTH_API_FAILED",
|
|
1742
|
+
"auth"
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
return res.json();
|
|
1746
|
+
}
|
|
1747
|
+
var BrokrAuthClient = class {
|
|
1748
|
+
constructor(_token, _gatewayUrl, appUrl) {
|
|
1749
|
+
this._appUrl = appUrl ?? (typeof process !== "undefined" ? process.env.BROKR_AUTH_URL : void 0);
|
|
1750
|
+
}
|
|
1751
|
+
// -------------------------------------------------------------------------
|
|
1752
|
+
// Identity — read user/session from incoming request
|
|
1753
|
+
// -------------------------------------------------------------------------
|
|
1754
|
+
/**
|
|
1755
|
+
* Get user from request headers. Returns null if not authenticated.
|
|
1756
|
+
* Calls the app's own Better Auth API.
|
|
1757
|
+
*/
|
|
1758
|
+
async user(headers) {
|
|
1759
|
+
const session = await this.session(headers);
|
|
1760
|
+
return session?.user ?? null;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Get user from request headers. Throws 401 if not authenticated.
|
|
1764
|
+
*/
|
|
1765
|
+
async requireUser(headers) {
|
|
1766
|
+
const u = await this.user(headers);
|
|
1767
|
+
if (!u) {
|
|
1768
|
+
throw new BrokrError("[brokr] Authentication required. The request has no valid session. Check that cookies are being forwarded.", "UNAUTHORIZED", "auth");
|
|
1769
|
+
}
|
|
1770
|
+
return u;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Get full session from request headers. Returns null if not authenticated.
|
|
1774
|
+
*/
|
|
1775
|
+
async session(headers) {
|
|
1776
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1777
|
+
const cookieHeader = headers.get("cookie") ?? "";
|
|
1778
|
+
if (!cookieHeader) return null;
|
|
1779
|
+
const res = await fetch(`${appUrl}/api/auth/get-session`, {
|
|
1780
|
+
method: "GET",
|
|
1781
|
+
headers: { cookie: cookieHeader }
|
|
1782
|
+
});
|
|
1783
|
+
if (!res.ok) return null;
|
|
1784
|
+
const data = await res.json();
|
|
1785
|
+
if (!data?.user || !data?.session) return null;
|
|
1786
|
+
return {
|
|
1787
|
+
user: mapSessionUser(data.user),
|
|
1788
|
+
sessionId: data.session.id,
|
|
1789
|
+
expiresAt: new Date(data.session.expiresAt)
|
|
485
1790
|
};
|
|
486
1791
|
}
|
|
487
|
-
|
|
488
|
-
|
|
1792
|
+
/**
|
|
1793
|
+
* Get full session. Throws 401 if not authenticated.
|
|
1794
|
+
*/
|
|
1795
|
+
async requireSession(headers) {
|
|
1796
|
+
const s = await this.session(headers);
|
|
1797
|
+
if (!s) {
|
|
1798
|
+
throw new BrokrError("[brokr] Authentication required. The request has no valid session. Check that cookies are being forwarded.", "UNAUTHORIZED", "auth");
|
|
1799
|
+
}
|
|
1800
|
+
return s;
|
|
1801
|
+
}
|
|
1802
|
+
// -------------------------------------------------------------------------
|
|
1803
|
+
// Legacy aliases (backward compat)
|
|
1804
|
+
// -------------------------------------------------------------------------
|
|
1805
|
+
/** @deprecated Use user(request.headers) instead. */
|
|
1806
|
+
async currentUser(request) {
|
|
1807
|
+
return this.user(request.headers);
|
|
1808
|
+
}
|
|
1809
|
+
/** @deprecated Use session(request.headers) instead. */
|
|
1810
|
+
async getSession(request) {
|
|
1811
|
+
return this.session(request.headers);
|
|
1812
|
+
}
|
|
1813
|
+
// -------------------------------------------------------------------------
|
|
1814
|
+
// Auth actions — call Better Auth endpoints on the stack's app
|
|
1815
|
+
// -------------------------------------------------------------------------
|
|
1816
|
+
/**
|
|
1817
|
+
* Sign in with email and password.
|
|
1818
|
+
*/
|
|
1819
|
+
async signIn(params) {
|
|
1820
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1821
|
+
const data = await authApiFetch(appUrl, "/api/auth/sign-in/email", {
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
body: params
|
|
1824
|
+
});
|
|
1825
|
+
return {
|
|
1826
|
+
user: mapSessionUser(data.user),
|
|
1827
|
+
sessionId: data.session.id,
|
|
1828
|
+
expiresAt: new Date(data.session.expiresAt)
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Sign in with OAuth provider. Returns redirect URL.
|
|
1833
|
+
*/
|
|
1834
|
+
async signInWithProvider(provider, options) {
|
|
1835
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1836
|
+
const redirectTo = options?.redirectTo ?? "/";
|
|
1837
|
+
if (!redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
|
|
1838
|
+
throw new BrokrError("[brokr] redirectTo must be a relative path (start with /)", "INVALID_REDIRECT", "auth");
|
|
1839
|
+
}
|
|
1840
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(provider)) {
|
|
1841
|
+
throw new BrokrError(`[brokr] Invalid provider name: "${provider}". Must be lowercase alphanumeric.`, "INVALID_PROVIDER", "auth");
|
|
1842
|
+
}
|
|
1843
|
+
const params = new URLSearchParams({ provider, callbackURL: redirectTo });
|
|
1844
|
+
return { redirectUrl: `${appUrl}/api/auth/sign-in/social?${params}` };
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Sign up with email and password.
|
|
1848
|
+
*/
|
|
1849
|
+
async signUp(params) {
|
|
1850
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1851
|
+
const data = await authApiFetch(appUrl, "/api/auth/sign-up/email", {
|
|
1852
|
+
method: "POST",
|
|
1853
|
+
body: params
|
|
1854
|
+
});
|
|
1855
|
+
return {
|
|
1856
|
+
user: mapSessionUser(data.user),
|
|
1857
|
+
sessionId: data.session.id,
|
|
1858
|
+
expiresAt: new Date(data.session.expiresAt)
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Sign out the current user.
|
|
1863
|
+
*/
|
|
1864
|
+
async signOut(headers) {
|
|
1865
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1866
|
+
const cookies = headers?.get("cookie") ?? "";
|
|
1867
|
+
await authApiFetch(appUrl, "/api/auth/sign-out", {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
cookies
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Send a magic link email.
|
|
1874
|
+
*/
|
|
1875
|
+
async sendMagicLink(email, options) {
|
|
1876
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1877
|
+
await authApiFetch(appUrl, "/api/auth/magic-link/send", {
|
|
1878
|
+
method: "POST",
|
|
1879
|
+
body: { email, callbackURL: options?.redirectTo ?? "/" }
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Send a password reset email.
|
|
1884
|
+
*/
|
|
1885
|
+
async sendPasswordReset(params) {
|
|
1886
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1887
|
+
await authApiFetch(appUrl, "/api/auth/forget-password", {
|
|
1888
|
+
method: "POST",
|
|
1889
|
+
body: params
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Reset password with token from email.
|
|
1894
|
+
*/
|
|
1895
|
+
async resetPassword(params) {
|
|
1896
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1897
|
+
await authApiFetch(appUrl, "/api/auth/reset-password", {
|
|
1898
|
+
method: "POST",
|
|
1899
|
+
body: { token: params.token, newPassword: params.password }
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Verify email with token from email.
|
|
1904
|
+
*/
|
|
1905
|
+
async verifyEmail(params) {
|
|
1906
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1907
|
+
await authApiFetch(appUrl, "/api/auth/verify-email", {
|
|
1908
|
+
method: "POST",
|
|
1909
|
+
body: params
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Update the current user's profile.
|
|
1914
|
+
*/
|
|
1915
|
+
async updateUser(params, headers) {
|
|
1916
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
1917
|
+
const cookies = headers?.get("cookie") ?? "";
|
|
1918
|
+
await authApiFetch(appUrl, "/api/auth/update-user", {
|
|
1919
|
+
method: "POST",
|
|
1920
|
+
body: params,
|
|
1921
|
+
cookies
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
// -------------------------------------------------------------------------
|
|
1925
|
+
// Session management
|
|
1926
|
+
// -------------------------------------------------------------------------
|
|
1927
|
+
/** Session management sub-client. */
|
|
1928
|
+
get sessions() {
|
|
1929
|
+
if (!this._sessions) {
|
|
1930
|
+
this._sessions = new BrokrSessionsClient(this._appUrl);
|
|
1931
|
+
}
|
|
1932
|
+
return this._sessions;
|
|
1933
|
+
}
|
|
1934
|
+
// -------------------------------------------------------------------------
|
|
1935
|
+
// Ownership — queries the stack's own DB via gateway → server
|
|
1936
|
+
// -------------------------------------------------------------------------
|
|
1937
|
+
/**
|
|
1938
|
+
* Check if the current user owns a resource. Throws 403 if not.
|
|
1939
|
+
*
|
|
1940
|
+
* Queries the developer's own database directly via DATABASE_URL.
|
|
1941
|
+
* Convention: pluralizes resource name → table, assumes `id` PK + `user_id` owner column.
|
|
1942
|
+
* Override with `opts.table` and `opts.ownerColumn` for non-standard schemas.
|
|
1943
|
+
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```ts
|
|
1946
|
+
* await brokr.auth.requireOwner('conversation', conversationId, request.headers);
|
|
1947
|
+
* // Under the hood: SELECT 1 FROM conversations WHERE id = $1 AND user_id = $2
|
|
1948
|
+
* ```
|
|
1949
|
+
*/
|
|
1950
|
+
async requireOwner(resource, id, headers, opts) {
|
|
1951
|
+
const isOwner = await this.isOwner(resource, id, headers, opts);
|
|
1952
|
+
if (!isOwner) {
|
|
1953
|
+
throw new BrokrError(
|
|
1954
|
+
`[brokr] Access denied \u2014 current user does not own this ${resource}. Verify the resource ID and that the user is the owner.`,
|
|
1955
|
+
"FORBIDDEN",
|
|
1956
|
+
"auth"
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Check if the current user owns a resource. Returns boolean.
|
|
1962
|
+
*
|
|
1963
|
+
* Runs a direct query against the developer's database (DATABASE_URL).
|
|
1964
|
+
* No gateway round-trip — this is a local DB operation.
|
|
1965
|
+
*/
|
|
1966
|
+
async isOwner(resource, id, headers, opts) {
|
|
1967
|
+
const user = await this.requireUser(headers);
|
|
1968
|
+
const dbUrl = typeof process !== "undefined" ? process.env.DATABASE_URL : void 0;
|
|
1969
|
+
if (!dbUrl) {
|
|
1970
|
+
throw new BrokrError(
|
|
1971
|
+
"[brokr] DATABASE_URL is not set. Cannot check ownership without a database.",
|
|
1972
|
+
"DB_NOT_CONFIGURED",
|
|
1973
|
+
"auth"
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
const tableName = opts?.table ?? pluralizeResource(resource);
|
|
1977
|
+
const ownerCol = opts?.ownerColumn ?? "user_id";
|
|
1978
|
+
validateSqlIdentifier(tableName, "table name");
|
|
1979
|
+
validateSqlIdentifier(ownerCol, "owner column");
|
|
1980
|
+
const pool = await getOwnershipPool(dbUrl);
|
|
1981
|
+
try {
|
|
1982
|
+
const result = await pool.query(
|
|
1983
|
+
`SELECT 1 FROM "${tableName}" WHERE id = $1 AND "${ownerCol}" = $2 LIMIT 1`,
|
|
1984
|
+
[id, user.id]
|
|
1985
|
+
);
|
|
1986
|
+
return (result.rowCount ?? 0) > 0;
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1989
|
+
console.error("[brokr] Ownership check error:", msg);
|
|
1990
|
+
if (msg.includes("does not exist")) {
|
|
1991
|
+
throw new BrokrError(
|
|
1992
|
+
`[brokr] Table "${tableName}" does not exist. Check your resource name or provide { table: '...' }.`,
|
|
1993
|
+
"TABLE_NOT_FOUND",
|
|
1994
|
+
"auth"
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
throw new BrokrError(
|
|
1998
|
+
"[brokr] Ownership check failed. Check your DATABASE_URL and that the database is accessible.",
|
|
1999
|
+
"DB_QUERY_FAILED",
|
|
2000
|
+
"auth"
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Check if the current user matches a specific userId. Throws 403 if not.
|
|
2006
|
+
*/
|
|
2007
|
+
async requireCurrentUser(userId, headers) {
|
|
2008
|
+
const user = await this.requireUser(headers);
|
|
2009
|
+
if (user.id !== userId) {
|
|
2010
|
+
throw new BrokrError("[brokr] Access denied.", "FORBIDDEN", "auth");
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
// -------------------------------------------------------------------------
|
|
2014
|
+
// Legacy alias
|
|
2015
|
+
// -------------------------------------------------------------------------
|
|
2016
|
+
/** @deprecated Use signInWithProvider() instead. */
|
|
2017
|
+
async getOAuthUrl(provider, options) {
|
|
2018
|
+
return this.signInWithProvider(provider, options);
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
var BrokrSessionsClient = class {
|
|
2022
|
+
constructor(appUrl) {
|
|
2023
|
+
this._appUrl = appUrl;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* List all active sessions for the current user.
|
|
2027
|
+
*/
|
|
2028
|
+
async list(headers) {
|
|
2029
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
2030
|
+
const cookies = headers.get("cookie") ?? "";
|
|
2031
|
+
const data = await authApiFetch(appUrl, "/api/auth/list-sessions", { cookies });
|
|
2032
|
+
const sessionToken = parseCookieValue(cookies, "better-auth.session_token") ?? parseCookieValue(cookies, "__Secure-better-auth.session_token");
|
|
2033
|
+
return data.map((s) => ({
|
|
2034
|
+
id: s.id,
|
|
2035
|
+
createdAt: new Date(s.createdAt),
|
|
2036
|
+
expiresAt: new Date(s.expiresAt),
|
|
2037
|
+
userAgent: s.userAgent,
|
|
2038
|
+
ipAddress: s.ipAddress,
|
|
2039
|
+
isCurrent: s.token === sessionToken
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Revoke a specific session by ID.
|
|
2044
|
+
*/
|
|
2045
|
+
async revoke(sessionId, headers) {
|
|
2046
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
2047
|
+
const cookies = headers.get("cookie") ?? "";
|
|
2048
|
+
await authApiFetch(appUrl, "/api/auth/revoke-session", {
|
|
2049
|
+
method: "POST",
|
|
2050
|
+
body: { id: sessionId },
|
|
2051
|
+
cookies
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Revoke all sessions except the current one.
|
|
2056
|
+
*/
|
|
2057
|
+
async revokeOthers(headers) {
|
|
2058
|
+
const appUrl = resolveAppUrl(this._appUrl);
|
|
2059
|
+
const cookies = headers.get("cookie") ?? "";
|
|
2060
|
+
await authApiFetch(appUrl, "/api/auth/revoke-other-sessions", {
|
|
2061
|
+
method: "POST",
|
|
2062
|
+
cookies
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
function parseCookies(cookieHeader) {
|
|
2067
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
2068
|
+
for (const pair of cookieHeader.split(";")) {
|
|
2069
|
+
const eqIdx = pair.indexOf("=");
|
|
2070
|
+
if (eqIdx === -1) continue;
|
|
2071
|
+
const name = pair.slice(0, eqIdx).trim();
|
|
2072
|
+
const value = pair.slice(eqIdx + 1).trim();
|
|
2073
|
+
if (name) cookies.set(name, value);
|
|
2074
|
+
}
|
|
2075
|
+
return cookies;
|
|
2076
|
+
}
|
|
2077
|
+
function parseCookieValue(cookieHeader, name) {
|
|
2078
|
+
return parseCookies(cookieHeader).get(name);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/models.ts
|
|
2082
|
+
var models = {
|
|
2083
|
+
/** Cheapest and fastest model (Deepseek Chat). */
|
|
2084
|
+
FAST: "deepseek-chat",
|
|
2085
|
+
/** Most capable model. */
|
|
2086
|
+
SMART: "claude-sonnet-4-6",
|
|
2087
|
+
/** Default balanced model (Deepseek Chat). */
|
|
2088
|
+
BALANCED: "deepseek-chat"
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
// src/runtime.ts
|
|
2092
|
+
var BrokrRuntime = class {
|
|
2093
|
+
constructor(options) {
|
|
2094
|
+
this._token = options?.token ?? resolveToken();
|
|
2095
|
+
this._gatewayUrl = options?.gatewayUrl ?? GATEWAY_URL;
|
|
2096
|
+
this.ai = new BrokrAIClient(this._token, this._gatewayUrl);
|
|
2097
|
+
this.storage = new BrokrStorageClient(this._token, this._gatewayUrl);
|
|
2098
|
+
this.email = new BrokrEmailClient(this._token, this._gatewayUrl);
|
|
2099
|
+
}
|
|
2100
|
+
// -------------------------------------------------------------------------
|
|
2101
|
+
// Lazy namespace getters
|
|
2102
|
+
// -------------------------------------------------------------------------
|
|
2103
|
+
/** Files client (upload + AI processing). */
|
|
2104
|
+
get files() {
|
|
2105
|
+
if (!this._files) {
|
|
2106
|
+
this._files = new BrokrFilesClient(this._token, this._gatewayUrl, this.storage);
|
|
2107
|
+
}
|
|
2108
|
+
return this._files;
|
|
2109
|
+
}
|
|
2110
|
+
/** Payments client (checkout, portal, plan queries). */
|
|
2111
|
+
get payments() {
|
|
2112
|
+
if (!this._payments) {
|
|
2113
|
+
this._payments = new BrokrPaymentsClient(this._token, this._gatewayUrl);
|
|
2114
|
+
}
|
|
2115
|
+
return this._payments;
|
|
2116
|
+
}
|
|
2117
|
+
/** Notifications client (send + list notifications). */
|
|
2118
|
+
get notifications() {
|
|
2119
|
+
if (!this._notifications) {
|
|
2120
|
+
this._notifications = new BrokrNotificationsClient(this._token, this._gatewayUrl);
|
|
2121
|
+
}
|
|
2122
|
+
return this._notifications;
|
|
2123
|
+
}
|
|
2124
|
+
/** Entitlements client (feature checks, usage queries). */
|
|
2125
|
+
get entitlements() {
|
|
2126
|
+
if (!this._entitlements) {
|
|
2127
|
+
this._entitlements = new BrokrEntitlementsClient(this._token, this._gatewayUrl);
|
|
2128
|
+
}
|
|
2129
|
+
return this._entitlements;
|
|
2130
|
+
}
|
|
2131
|
+
/** Auth client — lazily initialized. */
|
|
2132
|
+
get auth() {
|
|
2133
|
+
if (!this._auth) {
|
|
2134
|
+
this._auth = new BrokrAuthClient(this._token, this._gatewayUrl);
|
|
2135
|
+
}
|
|
2136
|
+
return this._auth;
|
|
2137
|
+
}
|
|
2138
|
+
// -------------------------------------------------------------------------
|
|
2139
|
+
// Root aliases — one-liner DX for the most common operations
|
|
2140
|
+
// -------------------------------------------------------------------------
|
|
2141
|
+
/** Send a chat message. Alias for `brokr.ai.chat()`. */
|
|
2142
|
+
chat(input, options) {
|
|
2143
|
+
return this.ai.chat(input, options);
|
|
2144
|
+
}
|
|
2145
|
+
/** Upload a file. Alias for `brokr.storage.upload()`. */
|
|
2146
|
+
upload(params) {
|
|
2147
|
+
return this.storage.upload(params);
|
|
2148
|
+
}
|
|
2149
|
+
/** Start a checkout flow. Alias for `brokr.payments.checkout()`. */
|
|
2150
|
+
checkout(params) {
|
|
2151
|
+
return this.payments.checkout(params);
|
|
2152
|
+
}
|
|
2153
|
+
/** Start a checkout flow. Alias for `brokr.checkout()` — north star one-liner. */
|
|
2154
|
+
purchase(params) {
|
|
2155
|
+
return this.payments.checkout(params);
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
function createBrokr(options) {
|
|
2159
|
+
BrokrDevConsole.install();
|
|
2160
|
+
initCapture({
|
|
2161
|
+
token: options?.token,
|
|
2162
|
+
gatewayUrl: options?.gatewayUrl
|
|
2163
|
+
});
|
|
2164
|
+
return new BrokrRuntime(options);
|
|
2165
|
+
}
|
|
489
2166
|
export {
|
|
490
2167
|
BrokrAIClient,
|
|
491
2168
|
BrokrAuthError,
|
|
492
2169
|
BrokrEmailClient,
|
|
2170
|
+
BrokrEntitlementsClient,
|
|
493
2171
|
BrokrError,
|
|
2172
|
+
BrokrFilesClient,
|
|
494
2173
|
BrokrNetworkError,
|
|
2174
|
+
BrokrNotFoundError,
|
|
2175
|
+
BrokrNotificationsClient,
|
|
2176
|
+
BrokrPaymentsClient,
|
|
495
2177
|
BrokrRateLimitError,
|
|
496
2178
|
BrokrRuntime,
|
|
497
2179
|
BrokrStorageClient,
|
|
2180
|
+
BrokrTimeoutError,
|
|
2181
|
+
BrokrValidationError,
|
|
498
2182
|
GATEWAY_URL,
|
|
499
2183
|
createBrokr,
|
|
2184
|
+
detectEnv,
|
|
2185
|
+
isDev,
|
|
2186
|
+
isProd,
|
|
2187
|
+
isStaging,
|
|
500
2188
|
models
|
|
501
2189
|
};
|