cloudbroker-sdk 0.5.0 → 0.6.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.
@@ -0,0 +1,209 @@
1
+ import 'dotenv/config';
2
+ import express from 'express';
3
+ import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
4
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
5
+ import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
6
+ import { CloudBrokerCloud } from 'cloudbroker-sdk';
7
+
8
+ const app = express();
9
+ app.use(express.json());
10
+
11
+ // ========== AWS Clients Setup (Wrapped) ==========
12
+
13
+ const s3 = CloudBrokerCloud.wrapAWS(new S3Client({ region: 'us-east-2' }), {
14
+ apiKey: process.env.CLOUDBROKER_API_KEY,
15
+ product: process.env.CLOUDBROKER_PRODUCT || 'checkout-example',
16
+ team: process.env.CLOUDBROKER_TEAM || 'platform',
17
+ });
18
+
19
+ const dynamodb = CloudBrokerCloud.wrapAWS(
20
+ DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'us-east-2' })),
21
+ {
22
+ apiKey: process.env.CLOUDBROKER_API_KEY,
23
+ product: process.env.CLOUDBROKER_PRODUCT || 'checkout-example',
24
+ team: process.env.CLOUDBROKER_TEAM || 'platform',
25
+ }
26
+ );
27
+
28
+ // ========== Routes ==========
29
+
30
+ /**
31
+ * Health check (no tracking)
32
+ */
33
+ app.get('/health', (req, res) => {
34
+ res.json({ ok: true, status: 'healthy', timestamp: new Date().toISOString() });
35
+ });
36
+
37
+ /**
38
+ * POST /orders — Create order
39
+ * Tracks: order-processing (DynamoDB), invoice-generation (S3)
40
+ */
41
+ app.post('/orders', async (req, res) => {
42
+ try {
43
+ const order = {
44
+ orderId: `order-${Date.now()}`,
45
+ customerId: req.body.customerId,
46
+ items: req.body.items || [],
47
+ total: req.body.total || 0,
48
+ status: 'pending',
49
+ createdAt: new Date().toISOString(),
50
+ };
51
+
52
+ // Feature 1: Persistir pedido no DynamoDB
53
+ await dynamodb.send(
54
+ new PutCommand({
55
+ TableName: 'CheckoutOrders',
56
+ Item: order,
57
+ }),
58
+ {
59
+ cbLabel: 'order-processing', // CloudBroker rastreia como "order-processing"
60
+ }
61
+ );
62
+
63
+ // Feature 2: Gerar invoice no S3
64
+ const invoiceJson = JSON.stringify({
65
+ orderId: order.orderId,
66
+ customerId: order.customerId,
67
+ items: order.items,
68
+ total: order.total,
69
+ createdAt: order.createdAt,
70
+ });
71
+
72
+ await s3.send(
73
+ new PutObjectCommand({
74
+ Bucket: 'checkout-invoices',
75
+ Key: `invoices/${order.orderId}.json`,
76
+ Body: invoiceJson,
77
+ ContentType: 'application/json',
78
+ }),
79
+ {
80
+ cbLabel: 'invoice-generation', // CloudBroker rastreia como "invoice-generation"
81
+ }
82
+ );
83
+
84
+ res.json({
85
+ ok: true,
86
+ orderId: order.orderId,
87
+ message: 'Order created successfully! Check CloudBroker dashboard for cost breakdown.',
88
+ });
89
+ } catch (error) {
90
+ console.error('[POST /orders] Error:', error.message);
91
+ res.status(500).json({ ok: false, error: error.message });
92
+ }
93
+ });
94
+
95
+ /**
96
+ * GET /orders/:id — Retrieve order
97
+ * Tracks: order-retrieval
98
+ */
99
+ app.get('/orders/:id', async (req, res) => {
100
+ try {
101
+ const result = await dynamodb.send(
102
+ new GetCommand({
103
+ TableName: 'CheckoutOrders',
104
+ Key: { orderId: req.params.id },
105
+ }),
106
+ {
107
+ cbLabel: 'order-retrieval',
108
+ }
109
+ );
110
+
111
+ res.json({
112
+ ok: true,
113
+ order: result.Item || null,
114
+ });
115
+ } catch (error) {
116
+ console.error('[GET /orders/:id] Error:', error.message);
117
+ res.status(500).json({ ok: false, error: error.message });
118
+ }
119
+ });
120
+
121
+ /**
122
+ * POST /verify-fraud — Fraud detection check
123
+ * Tracks: fraud-detection
124
+ */
125
+ app.post('/verify-fraud', async (req, res) => {
126
+ try {
127
+ const { orderId, customerId } = req.body;
128
+
129
+ // Simular: ler perfil de cliente do DynamoDB pra análise
130
+ const result = await dynamodb.send(
131
+ new GetCommand({
132
+ TableName: 'CustomerProfiles',
133
+ Key: { customerId },
134
+ }),
135
+ {
136
+ cbLabel: 'fraud-detection',
137
+ }
138
+ );
139
+
140
+ // Mock fraud score
141
+ const fraudScore = Math.random() * 0.5; // 0–0.5 = low risk
142
+ const isBlocked = fraudScore > 0.4;
143
+
144
+ res.json({
145
+ ok: true,
146
+ orderId,
147
+ customerId,
148
+ fraud_score: parseFloat(fraudScore.toFixed(2)),
149
+ is_blocked: isBlocked,
150
+ message: isBlocked ? 'Order blocked due to fraud risk' : 'Order cleared',
151
+ });
152
+ } catch (error) {
153
+ console.error('[POST /verify-fraud] Error:', error.message);
154
+ res.status(500).json({ ok: false, error: error.message });
155
+ }
156
+ });
157
+
158
+ /**
159
+ * POST /analytics — Track user action
160
+ * Tracks: analytics-tracking
161
+ */
162
+ app.post('/analytics', async (req, res) => {
163
+ try {
164
+ const event = {
165
+ eventId: `event-${Date.now()}`,
166
+ userId: req.body.userId,
167
+ eventType: req.body.eventType,
168
+ data: req.body.data || {},
169
+ timestamp: new Date().toISOString(),
170
+ };
171
+
172
+ // Store event in DynamoDB
173
+ await dynamodb.send(
174
+ new PutCommand({
175
+ TableName: 'AnalyticsEvents',
176
+ Item: event,
177
+ }),
178
+ {
179
+ cbLabel: 'analytics-tracking',
180
+ }
181
+ );
182
+
183
+ res.json({
184
+ ok: true,
185
+ eventId: event.eventId,
186
+ message: 'Analytics event recorded',
187
+ });
188
+ } catch (error) {
189
+ console.error('[POST /analytics] Error:', error.message);
190
+ res.status(500).json({ ok: false, error: error.message });
191
+ }
192
+ });
193
+
194
+ // ========== Server Start ==========
195
+
196
+ const PORT = parseInt(process.env.PORT || '3000', 10);
197
+
198
+ app.listen(PORT, () => {
199
+ console.log(`\n✅ Checkout Example App running on http://localhost:${PORT}`);
200
+ console.log(`📊 Check costs at: https://app.cloudbroker.app.br → Custo por Código\n`);
201
+ console.log('Features being tracked:');
202
+ console.log(' • order-processing (DynamoDB: create order)');
203
+ console.log(' • invoice-generation (S3: store invoice)');
204
+ console.log(' • order-retrieval (DynamoDB: fetch order)');
205
+ console.log(' • fraud-detection (DynamoDB: check profile)');
206
+ console.log(' • analytics-tracking (DynamoDB: log events)\n');
207
+ });
208
+
209
+ export default app;
@@ -0,0 +1,97 @@
1
+ import assert from 'assert';
2
+ import fetch from 'node-fetch';
3
+
4
+ const BASE_URL = 'http://localhost:3000';
5
+
6
+ async function test(name, fn) {
7
+ try {
8
+ await fn();
9
+ console.log(`✅ ${name}`);
10
+ } catch (error) {
11
+ console.error(`❌ ${name}`);
12
+ console.error(` ${error.message}`);
13
+ process.exit(1);
14
+ }
15
+ }
16
+
17
+ async function runTests() {
18
+ console.log('🧪 Running E2E tests...\n');
19
+
20
+ // Test 1: Health check
21
+ await test('Health check', async () => {
22
+ const res = await fetch(`${BASE_URL}/health`);
23
+ assert(res.ok, 'Health check failed');
24
+ const data = await res.json();
25
+ assert(data.ok === true, 'Health check returned ok: false');
26
+ });
27
+
28
+ // Test 2: Create order
29
+ await test('Create order', async () => {
30
+ const res = await fetch(`${BASE_URL}/orders`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({
34
+ customerId: 'test-cust-123',
35
+ items: [{ name: 'Test Widget', price: 99.99 }],
36
+ total: 99.99,
37
+ }),
38
+ });
39
+ assert(res.ok, `Create order failed: ${res.status}`);
40
+ const data = await res.json();
41
+ assert(data.ok === true, 'Create order returned ok: false');
42
+ assert(data.orderId, 'No orderId in response');
43
+ global.testOrderId = data.orderId; // Save for next test
44
+ });
45
+
46
+ // Test 3: Get order
47
+ await test('Get order', async () => {
48
+ assert(global.testOrderId, 'No orderId from previous test');
49
+ const res = await fetch(`${BASE_URL}/orders/${global.testOrderId}`);
50
+ assert(res.ok, `Get order failed: ${res.status}`);
51
+ const data = await res.json();
52
+ assert(data.ok === true, 'Get order returned ok: false');
53
+ // Order might not exist in DynamoDB (local/test setup), but the endpoint should work
54
+ });
55
+
56
+ // Test 4: Verify fraud
57
+ await test('Verify fraud', async () => {
58
+ const res = await fetch(`${BASE_URL}/verify-fraud`, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({
62
+ orderId: 'order-test-123',
63
+ customerId: 'cust-test-456',
64
+ }),
65
+ });
66
+ assert(res.ok, `Verify fraud failed: ${res.status}`);
67
+ const data = await res.json();
68
+ assert(data.ok === true, 'Verify fraud returned ok: false');
69
+ assert(typeof data.fraud_score === 'number', 'No fraud_score in response');
70
+ assert(data.fraud_score >= 0 && data.fraud_score <= 1, 'fraud_score out of range');
71
+ });
72
+
73
+ // Test 5: Analytics tracking
74
+ await test('Analytics tracking', async () => {
75
+ const res = await fetch(`${BASE_URL}/analytics`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ userId: 'user-test-789',
80
+ eventType: 'page_view',
81
+ data: { page: '/checkout' },
82
+ }),
83
+ });
84
+ assert(res.ok, `Analytics tracking failed: ${res.status}`);
85
+ const data = await res.json();
86
+ assert(data.ok === true, 'Analytics tracking returned ok: false');
87
+ assert(data.eventId, 'No eventId in response');
88
+ });
89
+
90
+ console.log('\n🎉 All tests passed!');
91
+ console.log('📊 Check CloudBroker dashboard in ~5 minutes to see the cost breakdown.');
92
+ }
93
+
94
+ runTests().catch((error) => {
95
+ console.error('Fatal error:', error.message);
96
+ process.exit(1);
97
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudbroker-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "CloudBroker SDK — Token Intelligence & Operational Financial Intelligence",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -8,9 +8,18 @@
8
8
  ".": "./src/index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node src/test.js"
11
+ "test": "node src/test.js && node src/test-azure-real.js"
12
12
  },
13
- "keywords": ["cloudbroker", "finops", "token-intelligence", "openai", "anthropic"],
13
+ "keywords": [
14
+ "cloudbroker",
15
+ "finops",
16
+ "token-intelligence",
17
+ "openai",
18
+ "anthropic"
19
+ ],
14
20
  "license": "MIT",
15
- "dependencies": {}
21
+ "devDependencies": {
22
+ "@azure/cosmos": "^4.10.0",
23
+ "@azure/storage-blob": "^12.33.0"
24
+ }
16
25
  }
package/src/client.js CHANGED
@@ -7,6 +7,17 @@ const DEFAULT_AI_ENDPOINT = "https://api.cloudbroker.app.br/api/ai/events";
7
7
  const DEFAULT_CLOUD_ENDPOINT = "https://api.cloudbroker.app.br/api/cloud/events";
8
8
  const DEFAULT_API_BASE = "https://api.cloudbroker.app.br/api";
9
9
  const RETRY_DELAY_MS = 30_000; // 30s entre retries
10
+
11
+ /**
12
+ * Retry que NÃO segura o event loop. Sem `.unref()`, um flush que falha deixa
13
+ * um timer de 30s pendurado que se reagenda — qualquer processo Node curto
14
+ * (Lambda, cron, CLI, suíte de teste) que use o SDK nunca encerra sozinho.
15
+ */
16
+ function scheduleRetry(fn) {
17
+ const t = setTimeout(fn, RETRY_DELAY_MS);
18
+ if (typeof t?.unref === "function") t.unref();
19
+ return t;
20
+ }
10
21
  const MAX_QUEUE_SIZE = 500; // máximo de eventos em memória
11
22
 
12
23
  export class CloudBrokerClient {
@@ -37,7 +48,7 @@ export class CloudBrokerClient {
37
48
  try {
38
49
  const payload = {
39
50
  ...event,
40
- sdkVersion: "0.5.0",
51
+ sdkVersion: "0.6.0",
41
52
  sentAt: new Date().toISOString(),
42
53
  };
43
54
  if (this.#cloudQueue.length < MAX_QUEUE_SIZE) {
@@ -69,7 +80,7 @@ export class CloudBrokerClient {
69
80
  if (!res.ok) {
70
81
  if (this.#debug) console.warn("[CloudBroker] cloud flush failed:", res.status);
71
82
  this.#cloudQueue.unshift(...batch);
72
- setTimeout(() => { this.#cloudFlushing = false; this.#flushCloud(); }, RETRY_DELAY_MS);
83
+ scheduleRetry(() => { this.#cloudFlushing = false; this.#flushCloud(); });
73
84
  return;
74
85
  }
75
86
 
@@ -77,7 +88,7 @@ export class CloudBrokerClient {
77
88
  } catch (err) {
78
89
  this.#cloudQueue.unshift(...batch);
79
90
  if (this.#debug) console.warn("[CloudBroker] cloud flush error:", err.message);
80
- setTimeout(() => { this.#cloudFlushing = false; this.#flushCloud(); }, RETRY_DELAY_MS);
91
+ scheduleRetry(() => { this.#cloudFlushing = false; this.#flushCloud(); });
81
92
  return;
82
93
  }
83
94
 
@@ -93,7 +104,7 @@ export class CloudBrokerClient {
93
104
  try {
94
105
  const payload = {
95
106
  ...event,
96
- sdkVersion: "0.5.0",
107
+ sdkVersion: "0.6.0",
97
108
  sentAt: new Date().toISOString(),
98
109
  };
99
110
 
@@ -184,7 +195,7 @@ export class CloudBrokerClient {
184
195
  // Recoloca na fila e agenda retry com backoff — nunca recursa na hora,
185
196
  // senão um erro persistente (ex: 401) martela o endpoint em loop apertado.
186
197
  this.#queue.unshift(...batch);
187
- setTimeout(() => { this.#flushing = false; this.#flush(); }, RETRY_DELAY_MS);
198
+ scheduleRetry(() => { this.#flushing = false; this.#flush(); });
188
199
  return;
189
200
  }
190
201
 
@@ -195,7 +206,7 @@ export class CloudBrokerClient {
195
206
  if (this.#debug) console.warn("[CloudBroker] flush error:", err.message);
196
207
 
197
208
  // Retry em 30s
198
- setTimeout(() => { this.#flushing = false; this.#flush(); }, RETRY_DELAY_MS);
209
+ scheduleRetry(() => { this.#flushing = false; this.#flush(); });
199
210
  return;
200
211
  }
201
212