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