business-stack 0.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/.python-version +1 -0
- package/backend/.env.example +65 -0
- package/backend/alembic/env.py +63 -0
- package/backend/alembic/script.py.mako +26 -0
- package/backend/alembic/versions/2a9c8f1d0e7b_multimodal_kb_schema.py +279 -0
- package/backend/alembic/versions/3c1d2e4f5a6b_sqlite_vec_embeddings.py +58 -0
- package/backend/alembic/versions/4e8b0c2d1a3f_document_links.py +50 -0
- package/backend/alembic/versions/6a0b1c2d3e4f_link_expansion_dedupe_columns.py +49 -0
- package/backend/alembic/versions/7d8e9f0a1b2c_document_chunks.py +70 -0
- package/backend/alembic/versions/8f2a1c0d9e3b_initial_empty_revision.py +22 -0
- package/backend/alembic/versions/9f0a1b2c3d4e_entity_mentions_cooccurrence.py +123 -0
- package/backend/alembic/versions/b1c2d3e4f5a6_pipeline_dedupe_dlq.py +99 -0
- package/backend/alembic/versions/c2d3e4f5061a_chat_sessions_messages.py +59 -0
- package/backend/alembic.ini +42 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/config.py +337 -0
- package/backend/app/connectors/__init__.py +13 -0
- package/backend/app/connectors/base.py +39 -0
- package/backend/app/connectors/builtins.py +51 -0
- package/backend/app/connectors/playwright_session.py +146 -0
- package/backend/app/connectors/registry.py +68 -0
- package/backend/app/connectors/thread_expansion/__init__.py +33 -0
- package/backend/app/connectors/thread_expansion/fakes.py +154 -0
- package/backend/app/connectors/thread_expansion/models.py +113 -0
- package/backend/app/connectors/thread_expansion/reddit.py +53 -0
- package/backend/app/connectors/thread_expansion/twitter.py +49 -0
- package/backend/app/db.py +5 -0
- package/backend/app/dependencies.py +34 -0
- package/backend/app/logging_config.py +35 -0
- package/backend/app/main.py +97 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/gateway_identity.py +17 -0
- package/backend/app/middleware/openapi_gateway.py +71 -0
- package/backend/app/middleware/request_id.py +23 -0
- package/backend/app/openapi_config.py +126 -0
- package/backend/app/routers/__init__.py +0 -0
- package/backend/app/routers/admin_pipeline.py +123 -0
- package/backend/app/routers/chat.py +206 -0
- package/backend/app/routers/chunks.py +36 -0
- package/backend/app/routers/entity_extract.py +31 -0
- package/backend/app/routers/example.py +8 -0
- package/backend/app/routers/gemini_embed.py +58 -0
- package/backend/app/routers/health.py +28 -0
- package/backend/app/routers/ingestion.py +146 -0
- package/backend/app/routers/link_expansion.py +34 -0
- package/backend/app/routers/pipeline_status.py +304 -0
- package/backend/app/routers/query.py +63 -0
- package/backend/app/routers/vectors.py +63 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/canonical.py +44 -0
- package/backend/app/schemas/chat.py +50 -0
- package/backend/app/schemas/ingest.py +29 -0
- package/backend/app/schemas/query.py +153 -0
- package/backend/app/schemas/vectors.py +56 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/chat_store.py +152 -0
- package/backend/app/services/chunking/__init__.py +3 -0
- package/backend/app/services/chunking/llm_boundaries.py +63 -0
- package/backend/app/services/chunking/schemas.py +30 -0
- package/backend/app/services/chunking/semantic_chunk.py +178 -0
- package/backend/app/services/chunking/splitters.py +214 -0
- package/backend/app/services/embeddings/__init__.py +20 -0
- package/backend/app/services/embeddings/build_inputs.py +140 -0
- package/backend/app/services/embeddings/dlq.py +128 -0
- package/backend/app/services/embeddings/gemini_api.py +207 -0
- package/backend/app/services/embeddings/persist.py +74 -0
- package/backend/app/services/embeddings/types.py +32 -0
- package/backend/app/services/embeddings/worker.py +224 -0
- package/backend/app/services/entities/__init__.py +12 -0
- package/backend/app/services/entities/gliner_extract.py +63 -0
- package/backend/app/services/entities/llm_extract.py +94 -0
- package/backend/app/services/entities/pipeline.py +179 -0
- package/backend/app/services/entities/spacy_extract.py +63 -0
- package/backend/app/services/entities/types.py +15 -0
- package/backend/app/services/gemini_chat.py +113 -0
- package/backend/app/services/hooks/__init__.py +3 -0
- package/backend/app/services/hooks/post_ingest.py +186 -0
- package/backend/app/services/ingestion/__init__.py +0 -0
- package/backend/app/services/ingestion/persist.py +188 -0
- package/backend/app/services/integrations_remote.py +91 -0
- package/backend/app/services/link_expansion/__init__.py +3 -0
- package/backend/app/services/link_expansion/canonical_url.py +45 -0
- package/backend/app/services/link_expansion/domain_policy.py +26 -0
- package/backend/app/services/link_expansion/html_extract.py +72 -0
- package/backend/app/services/link_expansion/rate_limit.py +32 -0
- package/backend/app/services/link_expansion/robots.py +46 -0
- package/backend/app/services/link_expansion/schemas.py +67 -0
- package/backend/app/services/link_expansion/worker.py +458 -0
- package/backend/app/services/normalization/__init__.py +7 -0
- package/backend/app/services/normalization/normalizer.py +331 -0
- package/backend/app/services/normalization/persist_normalized.py +67 -0
- package/backend/app/services/playwright_extract/__init__.py +13 -0
- package/backend/app/services/playwright_extract/__main__.py +96 -0
- package/backend/app/services/playwright_extract/extract.py +181 -0
- package/backend/app/services/retrieval_service.py +351 -0
- package/backend/app/sqlite_ext.py +36 -0
- package/backend/app/storage/__init__.py +3 -0
- package/backend/app/storage/blobs.py +30 -0
- package/backend/app/vectorstore/__init__.py +13 -0
- package/backend/app/vectorstore/sqlite_vec_store.py +242 -0
- package/backend/backend.egg-info/PKG-INFO +18 -0
- package/backend/backend.egg-info/SOURCES.txt +93 -0
- package/backend/backend.egg-info/dependency_links.txt +1 -0
- package/backend/backend.egg-info/entry_points.txt +2 -0
- package/backend/backend.egg-info/requires.txt +15 -0
- package/backend/backend.egg-info/top_level.txt +4 -0
- package/backend/package.json +15 -0
- package/backend/pyproject.toml +52 -0
- package/backend/tests/conftest.py +40 -0
- package/backend/tests/test_chat.py +92 -0
- package/backend/tests/test_chunking.py +132 -0
- package/backend/tests/test_entities.py +170 -0
- package/backend/tests/test_gemini_embed.py +224 -0
- package/backend/tests/test_health.py +24 -0
- package/backend/tests/test_ingest_raw.py +123 -0
- package/backend/tests/test_link_expansion.py +241 -0
- package/backend/tests/test_main.py +12 -0
- package/backend/tests/test_normalizer.py +114 -0
- package/backend/tests/test_openapi_gateway.py +40 -0
- package/backend/tests/test_pipeline_hardening.py +285 -0
- package/backend/tests/test_pipeline_status.py +71 -0
- package/backend/tests/test_playwright_extract.py +80 -0
- package/backend/tests/test_post_ingest_hooks.py +162 -0
- package/backend/tests/test_query.py +165 -0
- package/backend/tests/test_thread_expansion.py +72 -0
- package/backend/tests/test_vectors.py +85 -0
- package/backend/uv.lock +1839 -0
- package/bin/business-stack.cjs +412 -0
- package/frontend/web/.env.example +23 -0
- package/frontend/web/AGENTS.md +5 -0
- package/frontend/web/CLAUDE.md +1 -0
- package/frontend/web/README.md +36 -0
- package/frontend/web/components.json +25 -0
- package/frontend/web/next-env.d.ts +6 -0
- package/frontend/web/next.config.ts +30 -0
- package/frontend/web/package.json +65 -0
- package/frontend/web/postcss.config.mjs +7 -0
- package/frontend/web/skills-lock.json +35 -0
- package/frontend/web/src/app/account/[[...path]]/page.tsx +19 -0
- package/frontend/web/src/app/auth/[[...path]]/page.tsx +14 -0
- package/frontend/web/src/app/chat/page.tsx +725 -0
- package/frontend/web/src/app/favicon.ico +0 -0
- package/frontend/web/src/app/globals.css +563 -0
- package/frontend/web/src/app/layout.tsx +50 -0
- package/frontend/web/src/app/page.tsx +96 -0
- package/frontend/web/src/app/settings/integrations/actions.ts +74 -0
- package/frontend/web/src/app/settings/integrations/integrations-settings-form.tsx +330 -0
- package/frontend/web/src/app/settings/integrations/page.tsx +41 -0
- package/frontend/web/src/app/webhooks/alpha-alerts/route.ts +84 -0
- package/frontend/web/src/components/home-auth-panel.tsx +49 -0
- package/frontend/web/src/components/providers.tsx +50 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/registry.ts +35 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/types.ts +8 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge-delivery.test.ts +40 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge-delivery.ts +78 -0
- package/frontend/web/src/lib/alpha-webhook/connectors/wabridge.ts +30 -0
- package/frontend/web/src/lib/alpha-webhook/handler.ts +12 -0
- package/frontend/web/src/lib/alpha-webhook/signature.test.ts +33 -0
- package/frontend/web/src/lib/alpha-webhook/signature.ts +21 -0
- package/frontend/web/src/lib/alpha-webhook/types.ts +23 -0
- package/frontend/web/src/lib/auth-client.ts +23 -0
- package/frontend/web/src/lib/integrations-config.ts +125 -0
- package/frontend/web/src/lib/ui-utills.tsx +90 -0
- package/frontend/web/src/lib/utils.ts +6 -0
- package/frontend/web/tsconfig.json +36 -0
- package/frontend/web/tsconfig.tsbuildinfo +1 -0
- package/frontend/web/vitest.config.ts +14 -0
- package/gateway/.env.example +23 -0
- package/gateway/README.md +13 -0
- package/gateway/package.json +24 -0
- package/gateway/src/auth.ts +49 -0
- package/gateway/src/index.ts +141 -0
- package/gateway/src/integrations/admin.ts +19 -0
- package/gateway/src/integrations/crypto.ts +52 -0
- package/gateway/src/integrations/handlers.ts +124 -0
- package/gateway/src/integrations/keys.ts +12 -0
- package/gateway/src/integrations/store.ts +106 -0
- package/gateway/src/stack-secrets.ts +35 -0
- package/gateway/tsconfig.json +13 -0
- package/package.json +33 -0
- package/turbo.json +27 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { AlertCreatedPayload } from "../types";
|
|
3
|
+
import { formatAlertWhatsAppMessage } from "./wabridge-delivery";
|
|
4
|
+
|
|
5
|
+
const minimal: AlertCreatedPayload = {
|
|
6
|
+
_id: "674a1b2c3d4e5f6789012345",
|
|
7
|
+
alert_type: "price_spike",
|
|
8
|
+
date: "2025-04-01T10:30:00.000Z",
|
|
9
|
+
r2_key: "alerts/x.opus",
|
|
10
|
+
alert_string: "Notable price action on AAPL.",
|
|
11
|
+
symbol: "AAPL",
|
|
12
|
+
user_id: "u1",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("formatAlertWhatsAppMessage", () => {
|
|
16
|
+
it("includes symbol, type, body, date, and id", () => {
|
|
17
|
+
const text = formatAlertWhatsAppMessage(minimal);
|
|
18
|
+
expect(text).toContain("*AAPL*");
|
|
19
|
+
expect(text).toContain("price_spike");
|
|
20
|
+
expect(text).toContain("Notable price action");
|
|
21
|
+
expect(text).toContain("2025-04-01T10:30:00.000Z");
|
|
22
|
+
expect(text).toContain("674a1b2c3d4e5f6789012345");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("adds symbol_info lines when present", () => {
|
|
26
|
+
const withInfo: AlertCreatedPayload = {
|
|
27
|
+
...minimal,
|
|
28
|
+
symbol_info: {
|
|
29
|
+
symbol: "MSFT",
|
|
30
|
+
name: "Microsoft Corporation",
|
|
31
|
+
price: 410.25,
|
|
32
|
+
percentage_change: 1.42,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const text = formatAlertWhatsAppMessage(withInfo);
|
|
36
|
+
expect(text).toContain("Microsoft Corporation");
|
|
37
|
+
expect(text).toContain("410.25");
|
|
38
|
+
expect(text).toContain("+1.42%");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AlertCreatedPayload } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Plain-text body for WhatsApp (*bold* is optional WA formatting). */
|
|
4
|
+
export function formatAlertWhatsAppMessage(
|
|
5
|
+
payload: AlertCreatedPayload,
|
|
6
|
+
): string {
|
|
7
|
+
const parts: string[] = [];
|
|
8
|
+
const headline = payload.symbol
|
|
9
|
+
? `*${payload.symbol}* · ${payload.alert_type}`
|
|
10
|
+
: payload.alert_type;
|
|
11
|
+
parts.push(
|
|
12
|
+
headline,
|
|
13
|
+
"",
|
|
14
|
+
payload.alert_string,
|
|
15
|
+
"",
|
|
16
|
+
`🕐 ${payload.date}`,
|
|
17
|
+
`id: ${payload._id}`,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const info = payload.symbol_info;
|
|
21
|
+
if (info) {
|
|
22
|
+
if (info.name) parts.push("", info.name);
|
|
23
|
+
if (info.price != null) {
|
|
24
|
+
const ch =
|
|
25
|
+
info.percentage_change != null
|
|
26
|
+
? ` (${info.percentage_change >= 0 ? "+" : ""}${info.percentage_change}%)`
|
|
27
|
+
: "";
|
|
28
|
+
parts.push(`Price: ${info.price}${ch}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parts.join("\n").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let missingBaseUrlLogged = false;
|
|
36
|
+
|
|
37
|
+
export type WabridgeDeliveryOptions = {
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
phone?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* POSTs to a running [WABridge](https://github.com/marketcalls/wabridge) server.
|
|
44
|
+
* Uses options first, then `WABRIDGE_BASE_URL` / `WABRIDGE_PHONE` env.
|
|
45
|
+
*/
|
|
46
|
+
export async function sendWhatsAppViaWabridge(
|
|
47
|
+
message: string,
|
|
48
|
+
options?: WabridgeDeliveryOptions,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const rawBase =
|
|
51
|
+
options?.baseUrl?.trim() || process.env.WABRIDGE_BASE_URL?.trim();
|
|
52
|
+
if (!rawBase) {
|
|
53
|
+
if (!missingBaseUrlLogged) {
|
|
54
|
+
missingBaseUrlLogged = true;
|
|
55
|
+
console.warn(
|
|
56
|
+
"[wabridge] WABridge base URL not set; WhatsApp delivery skipped. Configure integration settings or WABRIDGE_BASE_URL.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const base = rawBase.replace(/\/$/, "");
|
|
63
|
+
const phone = options?.phone?.trim() || process.env.WABRIDGE_PHONE?.trim();
|
|
64
|
+
const path = phone ? "/send" : "/send/self";
|
|
65
|
+
const body = phone ? { phone, message } : { message };
|
|
66
|
+
|
|
67
|
+
const res = await fetch(`${base}${path}`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
signal: AbortSignal.timeout(25_000),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const text = await res.text();
|
|
76
|
+
throw new Error(`WABridge HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResolvedIntegrations } from "@/lib/integrations-config";
|
|
2
|
+
import type { AlertCreatedPayload } from "../types";
|
|
3
|
+
import type { AlertConnector } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
formatAlertWhatsAppMessage,
|
|
6
|
+
sendWhatsAppViaWabridge,
|
|
7
|
+
} from "./wabridge-delivery";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WhatsApp via [WABridge](https://github.com/marketcalls/wabridge) HTTP API.
|
|
11
|
+
* Disabled when resolved config has `wabridgeEnabled: false` (SQLite, env, or both).
|
|
12
|
+
*/
|
|
13
|
+
export function createWabridgeConnector(
|
|
14
|
+
cfg: ResolvedIntegrations,
|
|
15
|
+
): AlertConnector | null {
|
|
16
|
+
if (!cfg.wabridgeEnabled) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id: "wabridge",
|
|
22
|
+
async deliver(payload: AlertCreatedPayload) {
|
|
23
|
+
const message = formatAlertWhatsAppMessage(payload);
|
|
24
|
+
await sendWhatsAppViaWabridge(message, {
|
|
25
|
+
baseUrl: cfg.wabridgeBaseUrl,
|
|
26
|
+
phone: cfg.wabridgePhone,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { deliverAlertPayload } from "./connectors/registry";
|
|
2
|
+
import type { AlertCreatedPayload } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dispatches to registered alert connectors (see `connectors/registry.ts`).
|
|
6
|
+
* The webhook route awaits this before responding so serverless hosts finish outbound work.
|
|
7
|
+
*/
|
|
8
|
+
export async function handleAlertCreated(
|
|
9
|
+
payload: AlertCreatedPayload,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
await deliverAlertPayload(payload);
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { isValidAlphaWebhookSignature } from "./signature";
|
|
4
|
+
|
|
5
|
+
function sign(rawBody: string, secret: string): string {
|
|
6
|
+
return createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("isValidAlphaWebhookSignature", () => {
|
|
10
|
+
it("accepts a valid lowercase hex signature", () => {
|
|
11
|
+
const body =
|
|
12
|
+
'{"_id":"x","alert_type":"t","date":"d","r2_key":"k","alert_string":"s"}';
|
|
13
|
+
const secret = "my-secret";
|
|
14
|
+
const sig = sign(body, secret);
|
|
15
|
+
expect(isValidAlphaWebhookSignature(body, secret, sig)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("rejects wrong secret", () => {
|
|
19
|
+
const body = "{}";
|
|
20
|
+
const sig = sign(body, "a");
|
|
21
|
+
expect(isValidAlphaWebhookSignature(body, "b", sig)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects tampered body", () => {
|
|
25
|
+
const body = '{"a":1}';
|
|
26
|
+
const sig = sign(body, "s");
|
|
27
|
+
expect(isValidAlphaWebhookSignature('{"a":2}', "s", sig)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects length mismatch without throwing", () => {
|
|
31
|
+
expect(isValidAlphaWebhookSignature("{}", "s", "abc")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies X-Alpha-Signature: HMAC-SHA256(secret, raw UTF-8 body) as lowercase hex.
|
|
5
|
+
* Uses timing-safe comparison; mismatched lengths return false without calling timingSafeEqual.
|
|
6
|
+
*/
|
|
7
|
+
export function isValidAlphaWebhookSignature(
|
|
8
|
+
rawBody: string,
|
|
9
|
+
secret: string,
|
|
10
|
+
signatureHeader: string,
|
|
11
|
+
): boolean {
|
|
12
|
+
const expected = createHmac("sha256", secret)
|
|
13
|
+
.update(rawBody, "utf8")
|
|
14
|
+
.digest("hex");
|
|
15
|
+
const a = Buffer.from(expected, "utf8");
|
|
16
|
+
const b = Buffer.from(signatureHeader.trim().toLowerCase(), "utf8");
|
|
17
|
+
if (a.length !== b.length) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return timingSafeEqual(a, b);
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Payload for X-Alpha-Webhook-Event: alert.created */
|
|
2
|
+
export type AlertCreatedSymbolInfo = {
|
|
3
|
+
symbol: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
basic_industry?: string;
|
|
6
|
+
"52_week_low"?: number;
|
|
7
|
+
"52_week_high"?: number;
|
|
8
|
+
image?: string;
|
|
9
|
+
market_cap?: number;
|
|
10
|
+
price?: number;
|
|
11
|
+
percentage_change?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AlertCreatedPayload = {
|
|
15
|
+
_id: string;
|
|
16
|
+
alert_type: string;
|
|
17
|
+
date: string;
|
|
18
|
+
r2_key: string;
|
|
19
|
+
alert_string: string;
|
|
20
|
+
symbol?: string;
|
|
21
|
+
user_id?: string;
|
|
22
|
+
symbol_info?: AlertCreatedSymbolInfo;
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
function normalizeBaseUrl(url: string): string {
|
|
4
|
+
return url.replace(/\/$/, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function authBaseUrl(): string {
|
|
8
|
+
const raw = process.env.NEXT_PUBLIC_AUTH_BASE_URL?.trim();
|
|
9
|
+
if (raw) {
|
|
10
|
+
return normalizeBaseUrl(raw);
|
|
11
|
+
}
|
|
12
|
+
if (typeof window !== "undefined") {
|
|
13
|
+
return window.location.origin;
|
|
14
|
+
}
|
|
15
|
+
return "http://localhost:3000";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const authClient = createAuthClient({
|
|
19
|
+
baseURL: authBaseUrl(),
|
|
20
|
+
fetchOptions: {
|
|
21
|
+
credentials: "include",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
export type IntegrationsFromGateway = {
|
|
4
|
+
alphaWebhookSecret: string | null;
|
|
5
|
+
wabridgeBaseUrl: string | null;
|
|
6
|
+
wabridgePhone: string | null;
|
|
7
|
+
wabridgeEnabled: string | null;
|
|
8
|
+
geminiApiKey: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ResolvedIntegrations = {
|
|
12
|
+
alphaWebhookSecret: string | null;
|
|
13
|
+
wabridgeBaseUrl: string | undefined;
|
|
14
|
+
wabridgePhone: string | undefined;
|
|
15
|
+
wabridgeEnabled: boolean;
|
|
16
|
+
geminiApiKey: string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const gatewayInternal =
|
|
20
|
+
process.env.AUTH_GATEWAY_INTERNAL_URL?.trim() || "http://127.0.0.1:3001";
|
|
21
|
+
|
|
22
|
+
let cache: { at: number; value: IntegrationsFromGateway } | null = null;
|
|
23
|
+
const TTL_MS = 60_000;
|
|
24
|
+
|
|
25
|
+
export function invalidateIntegrationsConfigCache(): void {
|
|
26
|
+
cache = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function envFallback(): IntegrationsFromGateway {
|
|
30
|
+
const wabridgeEnabledEnv = process.env.WABRIDGE_ENABLED?.trim();
|
|
31
|
+
let wabridgeEnabled: string | null = null;
|
|
32
|
+
if (wabridgeEnabledEnv === "0" || wabridgeEnabledEnv === "false") {
|
|
33
|
+
wabridgeEnabled = "false";
|
|
34
|
+
} else if (wabridgeEnabledEnv === "1" || wabridgeEnabledEnv === "true") {
|
|
35
|
+
wabridgeEnabled = "true";
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
alphaWebhookSecret: process.env.ALPHA_WEBHOOK_SECRET?.trim() ?? null,
|
|
39
|
+
wabridgeBaseUrl: process.env.WABRIDGE_BASE_URL?.trim() ?? null,
|
|
40
|
+
wabridgePhone: process.env.WABRIDGE_PHONE?.trim() ?? null,
|
|
41
|
+
wabridgeEnabled,
|
|
42
|
+
geminiApiKey: process.env.GEMINI_API_KEY?.trim() ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchFromGateway(): Promise<IntegrationsFromGateway | null> {
|
|
47
|
+
const bearer = process.env.INTEGRATIONS_INTERNAL_SECRET?.trim();
|
|
48
|
+
if (!bearer) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${gatewayInternal}/internal/integrations`, {
|
|
53
|
+
headers: { Authorization: `Bearer ${bearer}` },
|
|
54
|
+
cache: "no-store",
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return (await res.json()) as IntegrationsFromGateway;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Plaintext integration values for server routes (webhooks, connectors). Cached briefly. */
|
|
66
|
+
export async function getIntegrationsConfig(): Promise<ResolvedIntegrations> {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
let raw: IntegrationsFromGateway;
|
|
69
|
+
|
|
70
|
+
if (cache && now - cache.at < TTL_MS) {
|
|
71
|
+
raw = cache.value;
|
|
72
|
+
} else {
|
|
73
|
+
const fromGateway = await fetchFromGateway();
|
|
74
|
+
if (fromGateway) {
|
|
75
|
+
raw = fromGateway;
|
|
76
|
+
} else {
|
|
77
|
+
raw = envFallback();
|
|
78
|
+
}
|
|
79
|
+
cache = { at: now, value: raw };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const envFb = envFallback();
|
|
83
|
+
const alpha =
|
|
84
|
+
raw.alphaWebhookSecret?.trim() || envFb.alphaWebhookSecret?.trim() || null;
|
|
85
|
+
const base =
|
|
86
|
+
raw.wabridgeBaseUrl?.trim() || envFb.wabridgeBaseUrl?.trim() || undefined;
|
|
87
|
+
const phone =
|
|
88
|
+
raw.wabridgePhone?.trim() || envFb.wabridgePhone?.trim() || undefined;
|
|
89
|
+
|
|
90
|
+
const wabridgeEnabled = resolveWabridgeEnabled(
|
|
91
|
+
raw.wabridgeEnabled,
|
|
92
|
+
envFb.wabridgeEnabled,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const gemini = raw.geminiApiKey?.trim() || envFb.geminiApiKey?.trim() || null;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
alphaWebhookSecret: alpha,
|
|
99
|
+
wabridgeBaseUrl: base,
|
|
100
|
+
wabridgePhone: phone,
|
|
101
|
+
wabridgeEnabled,
|
|
102
|
+
geminiApiKey: gemini,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveWabridgeEnabled(
|
|
107
|
+
db: string | null,
|
|
108
|
+
env: string | null,
|
|
109
|
+
): boolean {
|
|
110
|
+
const envRaw = process.env.WABRIDGE_ENABLED?.trim();
|
|
111
|
+
if (envRaw === "0" || envRaw === "false") {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (envRaw === "1" || envRaw === "true") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const merged = db ?? env;
|
|
118
|
+
if (merged === "false") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (merged === "true") {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
|
|
4
|
+
type MaxWidthContainerProps<T extends ElementType = "main"> = {
|
|
5
|
+
/**
|
|
6
|
+
* The HTML element to render. Defaults to "main".
|
|
7
|
+
*/
|
|
8
|
+
as?: T;
|
|
9
|
+
/**
|
|
10
|
+
* Maximum width variant. Defaults to "2xl".
|
|
11
|
+
* - "sm": max-w-screen-sm (640px)
|
|
12
|
+
* - "md": max-w-screen-md (768px)
|
|
13
|
+
* - "lg": max-w-screen-lg (1024px)
|
|
14
|
+
* - "xl": max-w-screen-xl (1280px)
|
|
15
|
+
* - "2xl": max-w-screen-2xl (1536px)
|
|
16
|
+
* - "full": no max-width constraint
|
|
17
|
+
*/
|
|
18
|
+
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
|
19
|
+
/**
|
|
20
|
+
* Padding variant. Defaults to "default".
|
|
21
|
+
* - "none": no padding
|
|
22
|
+
* - "sm": px-2.5 (10px)
|
|
23
|
+
* - "default": px-2.5 md:px-20 (10px mobile, 80px desktop)
|
|
24
|
+
* - "lg": px-4 md:px-24 (16px mobile, 96px desktop)
|
|
25
|
+
*/
|
|
26
|
+
padding?: "none" | "sm" | "default" | "lg";
|
|
27
|
+
/**
|
|
28
|
+
* Additional CSS classes to apply.
|
|
29
|
+
*/
|
|
30
|
+
className?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Child elements to render inside the container.
|
|
33
|
+
*/
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
} & ComponentPropsWithoutRef<T>;
|
|
36
|
+
|
|
37
|
+
const maxWidthClasses = {
|
|
38
|
+
sm: "max-w-screen-sm",
|
|
39
|
+
md: "max-w-screen-md",
|
|
40
|
+
lg: "max-w-screen-lg",
|
|
41
|
+
xl: "max-w-screen-xl",
|
|
42
|
+
"2xl": "max-w-screen-2xl",
|
|
43
|
+
full: "",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
const paddingClasses = {
|
|
47
|
+
none: "",
|
|
48
|
+
sm: "px-2.5",
|
|
49
|
+
default: "px-2.5 md:px-20",
|
|
50
|
+
lg: "px-4 md:px-24",
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A flexible container component that centers content with a maximum width.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <MaxWidthContainer>
|
|
59
|
+
* <h1>Centered content</h1>
|
|
60
|
+
* </MaxWidthContainer>
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* <MaxWidthContainer as="div" maxWidth="lg" padding="sm">
|
|
66
|
+
* <p>Custom container</p>
|
|
67
|
+
* </MaxWidthContainer>
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export default function MaxWidthContainer<T extends ElementType = "main">({
|
|
71
|
+
as,
|
|
72
|
+
maxWidth = "2xl",
|
|
73
|
+
padding = "default",
|
|
74
|
+
className,
|
|
75
|
+
children,
|
|
76
|
+
...props
|
|
77
|
+
}: MaxWidthContainerProps<T>) {
|
|
78
|
+
const Component = as ?? ("main" as ElementType);
|
|
79
|
+
const maxWidthClass = maxWidthClasses[maxWidth];
|
|
80
|
+
const paddingClass = paddingClasses[padding];
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Component
|
|
84
|
+
className={cn("mx-auto w-full", maxWidthClass, paddingClass, className)}
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</Component>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"],
|
|
23
|
+
"~/*": ["./src/*"]
|
|
24
|
+
},
|
|
25
|
+
"strictNullChecks": true
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"next-env.d.ts",
|
|
29
|
+
"**/*.ts",
|
|
30
|
+
"**/*.tsx",
|
|
31
|
+
".next/types/**/*.ts",
|
|
32
|
+
".next/dev/types/**/*.ts",
|
|
33
|
+
"**/*.mts"
|
|
34
|
+
],
|
|
35
|
+
"exclude": ["node_modules"]
|
|
36
|
+
}
|