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.
- package/.github/workflows/publish.yml +18 -0
- package/README.md +83 -5
- package/docs/COST_PER_FEATURE.md +296 -0
- package/example-checkout-app/.env.example +13 -0
- package/example-checkout-app/README.md +225 -0
- package/example-checkout-app/package.json +28 -0
- package/example-checkout-app/server.js +209 -0
- package/example-checkout-app/test/e2e.js +97 -0
- package/package.json +13 -4
- package/src/client.js +17 -6
- package/src/cloud-wrapper.js +531 -5
- package/src/pricing-cloud.js +165 -10
- package/src/test-azure-real.js +169 -0
- package/src/test.js +264 -0
|
@@ -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.
|
|
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": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cloudbroker",
|
|
15
|
+
"finops",
|
|
16
|
+
"token-intelligence",
|
|
17
|
+
"openai",
|
|
18
|
+
"anthropic"
|
|
19
|
+
],
|
|
14
20
|
"license": "MIT",
|
|
15
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
scheduleRetry(() => { this.#flushing = false; this.#flush(); });
|
|
199
210
|
return;
|
|
200
211
|
}
|
|
201
212
|
|