@proulr/bff-core 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/README.md +15 -0
- package/package.json +36 -0
- package/src/bff-env-loader.ts +22 -0
- package/src/bff-metrics.ts +35 -0
- package/src/bff-supabase-client.ts +33 -0
- package/src/index.ts +130 -0
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@proulr/bff-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/saviorbp/proulr-bff-core.git"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run typecheck"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hono/node-server": "^1.19.6",
|
|
26
|
+
"@proulr/sdui-contract": "^0.1.0",
|
|
27
|
+
"@supabase/supabase-js": "^2.107.0",
|
|
28
|
+
"dotenv": "^16.6.1",
|
|
29
|
+
"hono": "^4.10.6",
|
|
30
|
+
"prom-client": "^15.1.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.10.0",
|
|
34
|
+
"typescript": "~6.0.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
|
|
5
|
+
let loaded = false;
|
|
6
|
+
|
|
7
|
+
/** Loads `.env` from the BFF cwd, or falls back to `proulr-app/.env` in local dev. */
|
|
8
|
+
export function loadBffEnv(): void {
|
|
9
|
+
if (loaded) return;
|
|
10
|
+
loaded = true;
|
|
11
|
+
|
|
12
|
+
const cwdEnv = resolve(process.cwd(), '.env');
|
|
13
|
+
if (existsSync(cwdEnv)) {
|
|
14
|
+
config({ path: cwdEnv });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const appEnv = resolve(process.cwd(), '../proulr-app/.env');
|
|
19
|
+
if (existsSync(appEnv)) {
|
|
20
|
+
config({ path: appEnv });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Counter, Histogram, Registry, collectDefaultMetrics } from 'prom-client';
|
|
2
|
+
|
|
3
|
+
export type BffMetrics = {
|
|
4
|
+
registry: Registry;
|
|
5
|
+
requestDuration: Histogram<'bff_id' | 'screen_id' | 'status' | 'method'>;
|
|
6
|
+
requestsTotal: Counter<'bff_id' | 'screen_id' | 'status' | 'method'>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createBffMetrics(bffId: string): BffMetrics {
|
|
10
|
+
const registry = new Registry();
|
|
11
|
+
registry.setDefaultLabels({ bff_id: bffId });
|
|
12
|
+
|
|
13
|
+
collectDefaultMetrics({ register: registry, prefix: 'bff_' });
|
|
14
|
+
|
|
15
|
+
const requestDuration = new Histogram({
|
|
16
|
+
name: 'bff_http_request_duration_seconds',
|
|
17
|
+
help: 'BFF HTTP request duration in seconds',
|
|
18
|
+
labelNames: ['bff_id', 'screen_id', 'status', 'method'] as const,
|
|
19
|
+
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
20
|
+
registers: [registry],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const requestsTotal = new Counter({
|
|
24
|
+
name: 'bff_http_requests_total',
|
|
25
|
+
help: 'Total BFF HTTP requests',
|
|
26
|
+
labelNames: ['bff_id', 'screen_id', 'status', 'method'] as const,
|
|
27
|
+
registers: [registry],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { registry, requestDuration, requestsTotal };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isBffMetricsEnabled(): boolean {
|
|
34
|
+
return process.env.BFF_METRICS !== '0';
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { loadBffEnv } from './bff-env-loader';
|
|
3
|
+
|
|
4
|
+
loadBffEnv();
|
|
5
|
+
|
|
6
|
+
const supabaseUrl = process.env.SUPABASE_URL ?? process.env.EXPO_PUBLIC_SUPABASE_URL ?? '';
|
|
7
|
+
const supabaseAnonKey =
|
|
8
|
+
process.env.SUPABASE_ANON_KEY ?? process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '';
|
|
9
|
+
|
|
10
|
+
export function isBffSupabaseConfigured(): boolean {
|
|
11
|
+
return Boolean(supabaseUrl && supabaseAnonKey);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createBffSupabaseClient(authToken?: string): SupabaseClient {
|
|
15
|
+
if (!isBffSupabaseConfigured()) {
|
|
16
|
+
throw new Error('bff_supabase_not_configured');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return createClient(supabaseUrl, supabaseAnonKey, {
|
|
20
|
+
auth: {
|
|
21
|
+
persistSession: false,
|
|
22
|
+
autoRefreshToken: false,
|
|
23
|
+
detectSessionInUrl: false,
|
|
24
|
+
},
|
|
25
|
+
global: authToken
|
|
26
|
+
? {
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${authToken}`,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
: undefined,
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { serve } from '@hono/node-server';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { cors } from 'hono/cors';
|
|
5
|
+
import type { SduiScreenBuilder, SduiScreenBuilderContext } from '@proulr/sdui-contract';
|
|
6
|
+
import { createBffMetrics, isBffMetricsEnabled } from './bff-metrics';
|
|
7
|
+
|
|
8
|
+
export type CreateSduiBffOptions = {
|
|
9
|
+
name: string;
|
|
10
|
+
port: number;
|
|
11
|
+
bffId: string;
|
|
12
|
+
screens: Record<string, SduiScreenBuilder>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function buildContext(
|
|
16
|
+
routeParams: Record<string, string>,
|
|
17
|
+
query: Record<string, string>,
|
|
18
|
+
authToken?: string,
|
|
19
|
+
): SduiScreenBuilderContext {
|
|
20
|
+
return { routeParams, query, authToken };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readAuthToken(headerValue: string | undefined): string | undefined {
|
|
24
|
+
if (!headerValue) return undefined;
|
|
25
|
+
const match = headerValue.match(/^Bearer\s+(.+)$/i);
|
|
26
|
+
return match?.[1]?.trim() || undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveScreenId(path: string): string {
|
|
30
|
+
if (path.startsWith('/v1/screens/')) return path.slice('/v1/screens/'.length);
|
|
31
|
+
if (path.startsWith('/v1/actions/')) return `action:${path.slice('/v1/actions/'.length)}`;
|
|
32
|
+
if (path === '/health') return 'health';
|
|
33
|
+
if (path === '/metrics') return 'metrics';
|
|
34
|
+
return 'other';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createSduiBff(options: CreateSduiBffOptions) {
|
|
38
|
+
const app = new Hono();
|
|
39
|
+
const metricsEnabled = isBffMetricsEnabled();
|
|
40
|
+
const metrics = metricsEnabled ? createBffMetrics(options.bffId) : null;
|
|
41
|
+
|
|
42
|
+
app.use('*', cors());
|
|
43
|
+
|
|
44
|
+
app.use('*', async (c, next) => {
|
|
45
|
+
const requestId = randomUUID();
|
|
46
|
+
const startedAt = performance.now();
|
|
47
|
+
c.header('X-Request-Id', requestId);
|
|
48
|
+
|
|
49
|
+
await next();
|
|
50
|
+
|
|
51
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
52
|
+
const path = new URL(c.req.url).pathname;
|
|
53
|
+
const screenId = resolveScreenId(path);
|
|
54
|
+
const status = String(c.res.status);
|
|
55
|
+
const method = c.req.method;
|
|
56
|
+
|
|
57
|
+
if (metrics) {
|
|
58
|
+
const labels = { bff_id: options.bffId, screen_id: screenId, status, method };
|
|
59
|
+
metrics.requestDuration.observe(labels, durationMs / 1000);
|
|
60
|
+
metrics.requestsTotal.inc(labels);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
type: 'bff_request',
|
|
66
|
+
request_id: requestId,
|
|
67
|
+
bff_id: options.bffId,
|
|
68
|
+
screen_id: screenId,
|
|
69
|
+
method: c.req.method,
|
|
70
|
+
path,
|
|
71
|
+
status: c.res.status,
|
|
72
|
+
duration_ms: durationMs,
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (metrics) {
|
|
78
|
+
app.get('/metrics', async (c) => {
|
|
79
|
+
c.header('Content-Type', metrics.registry.contentType);
|
|
80
|
+
return c.body(await metrics.registry.metrics());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
app.get('/health', (c) =>
|
|
85
|
+
c.json({
|
|
86
|
+
ok: true,
|
|
87
|
+
bff: options.bffId,
|
|
88
|
+
name: options.name,
|
|
89
|
+
screens: Object.keys(options.screens),
|
|
90
|
+
metrics_enabled: metricsEnabled,
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
app.get('/v1/screens/:screenId', async (c) => {
|
|
95
|
+
const screenId = c.req.param('screenId');
|
|
96
|
+
const builder = options.screens[screenId];
|
|
97
|
+
|
|
98
|
+
if (!builder) {
|
|
99
|
+
return c.json({ error: 'screen_not_found', screen_id: screenId }, 404);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const query = Object.fromEntries(new URL(c.req.url).searchParams.entries());
|
|
103
|
+
const authToken = readAuthToken(c.req.header('Authorization'));
|
|
104
|
+
const screen = await builder(buildContext({}, query, authToken));
|
|
105
|
+
return c.json(screen);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
app.post('/v1/actions/:actionId', async (c) => {
|
|
109
|
+
const actionId = c.req.param('actionId');
|
|
110
|
+
const body = await c.req.json().catch(() => ({}));
|
|
111
|
+
return c.json({
|
|
112
|
+
ok: true,
|
|
113
|
+
action_id: actionId,
|
|
114
|
+
received: body,
|
|
115
|
+
message: 'Action endpoint stub — implement per domain in each BFF.',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
app,
|
|
121
|
+
start() {
|
|
122
|
+
serve({ fetch: app.fetch, port: options.port, hostname: '0.0.0.0' }, () => {
|
|
123
|
+
console.log(`[${options.bffId}] listening on http://0.0.0.0:${options.port}`);
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export * from '@proulr/sdui-contract';
|
|
130
|
+
export { createBffSupabaseClient, isBffSupabaseConfigured } from './bff-supabase-client';
|