@dotbots-boutique/server-sdk 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 +188 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +129 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -0
- package/dist/proxy/app-secrets.d.ts +14 -0
- package/dist/proxy/app-secrets.js +41 -0
- package/dist/proxy/handlers.d.ts +21 -0
- package/dist/proxy/handlers.js +137 -0
- package/dist/proxy/index.d.ts +3 -0
- package/dist/proxy/index.js +11 -0
- package/dist/proxy/types.d.ts +29 -0
- package/dist/proxy/types.js +2 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @dotbots-boutique/server-sdk
|
|
2
|
+
|
|
3
|
+
The official backend SDK for [DotBots Boutique](https://dotbots.boutique). Use this package to make AI calls, trigger payments and retrieve user information from your Deno backend — without a browser or user session.
|
|
4
|
+
|
|
5
|
+
## When to use this SDK
|
|
6
|
+
|
|
7
|
+
Use the backend SDK when logic must run server-side:
|
|
8
|
+
|
|
9
|
+
- Webhook handlers
|
|
10
|
+
- Background jobs and scheduled tasks
|
|
11
|
+
- Multi-step backend processing flows
|
|
12
|
+
- Any operation that runs without a user actively waiting in the browser
|
|
13
|
+
|
|
14
|
+
For user-facing interactions in the browser, use the frontend SDK (`@dotbots-boutique/auth-sdk`).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @dotbots-boutique/server-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or in Deno:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { DotBotsBackend } from 'npm:@dotbots-boutique/server-sdk';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Environment variables
|
|
29
|
+
|
|
30
|
+
These are injected automatically by the DotBots platform — no manual configuration needed:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
DOTBOTS_APP_ID=uuid
|
|
34
|
+
DOTBOTS_APP_SECRET=...
|
|
35
|
+
DOTBOTS_API_URL=https://api.dotbots.ai
|
|
36
|
+
ENVIRONMENT=test|prod
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
Initialise once at app startup, before handling any requests:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { DotBotsBackend } from 'npm:@dotbots-boutique/server-sdk';
|
|
45
|
+
|
|
46
|
+
const dotbots = new DotBotsBackend({
|
|
47
|
+
appId: Deno.env.get('DOTBOTS_APP_ID')!,
|
|
48
|
+
appSecret: Deno.env.get('DOTBOTS_APP_SECRET')!,
|
|
49
|
+
apiUrl: Deno.env.get('DOTBOTS_API_URL') ?? 'https://api.dotbots.ai',
|
|
50
|
+
environment: Deno.env.get('ENVIRONMENT') ?? 'prod'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Must be called before any other methods
|
|
54
|
+
await dotbots.initialize();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`initialize()` fetches the proxy URL from the platform. It must complete before any AI calls, payments or user lookups are made.
|
|
58
|
+
|
|
59
|
+
## AI calls
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// orgId required when feature is configured as paidBy = 'org' in the platform (default)
|
|
63
|
+
const response = await dotbots.ai('generate-summary', {
|
|
64
|
+
messages: [
|
|
65
|
+
{ role: 'user', content: 'Summarise the following document: ...' }
|
|
66
|
+
],
|
|
67
|
+
orgId: user.orgId
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(response.content);
|
|
71
|
+
console.log(`Cost: ${response.cost.tokens} tokens`);
|
|
72
|
+
|
|
73
|
+
// Feature configured as paidBy = 'app' in platform — no orgId needed
|
|
74
|
+
const response = await dotbots.ai('internal-classification', {
|
|
75
|
+
messages: [{ role: 'user', content: 'Classify this text...' }]
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Streaming
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
let result = '';
|
|
83
|
+
|
|
84
|
+
await dotbots.aiStream(
|
|
85
|
+
'generate-text',
|
|
86
|
+
{
|
|
87
|
+
messages: [{ role: 'user', content: 'Write a report...' }],
|
|
88
|
+
orgId: user.orgId
|
|
89
|
+
},
|
|
90
|
+
(delta) => {
|
|
91
|
+
result += delta;
|
|
92
|
+
// optionally forward to client via SSE
|
|
93
|
+
},
|
|
94
|
+
(finalResponse) => {
|
|
95
|
+
console.log(`Completed — cost: ${finalResponse.cost.tokens} tokens`);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Payments
|
|
101
|
+
|
|
102
|
+
The `paidBy` value is configured per feature in the platform — default is `org`. Your code only needs to provide `orgId` when the feature charges the organisation:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Feature paidBy = 'org' (default) — provide orgId
|
|
106
|
+
await dotbots.charge('data-export', {
|
|
107
|
+
orgId: user.orgId,
|
|
108
|
+
quantity: 1
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Feature paidBy = 'app' — no orgId needed
|
|
112
|
+
await dotbots.charge('internal-job', {
|
|
113
|
+
quantity: 1
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Getting user info
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const user = await dotbots.getUser(userId);
|
|
121
|
+
console.log(user.name, user.orgId, user.roles);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Error handling
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { DotBotsBackend, DotBotsBackendError } from 'npm:@dotbots-boutique/server-sdk';
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await dotbots.ai('generate-text', { messages, orgId });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof DotBotsBackendError) {
|
|
133
|
+
switch (error.code) {
|
|
134
|
+
case 'AI_INSUFFICIENT_BALANCE':
|
|
135
|
+
// Org has no tokens — notify user
|
|
136
|
+
break;
|
|
137
|
+
case 'AI_FEATURE_NOT_FOUND':
|
|
138
|
+
// Feature not configured in platform — developer error
|
|
139
|
+
console.error('AI feature not configured in DotBots platform');
|
|
140
|
+
break;
|
|
141
|
+
case 'AI_CALL_FAILED':
|
|
142
|
+
// Provider error — retry or fallback
|
|
143
|
+
break;
|
|
144
|
+
case 'UNAUTHORIZED':
|
|
145
|
+
// Invalid app secret — check DOTBOTS_APP_SECRET env variable
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Error codes
|
|
153
|
+
|
|
154
|
+
| Code | Description |
|
|
155
|
+
|------|-------------|
|
|
156
|
+
| `AI_FEATURE_NOT_FOUND` | Feature not configured in the platform |
|
|
157
|
+
| `AI_PROVIDER_NOT_CONFIGURED` | API key not set for the AI provider |
|
|
158
|
+
| `AI_INSUFFICIENT_BALANCE` | Organisation balance is 0 and awaiting top-up |
|
|
159
|
+
| `AI_CALL_FAILED` | AI provider returned an error |
|
|
160
|
+
| `CHARGE_FAILED` | Payment failed |
|
|
161
|
+
| `USER_NOT_FOUND` | User not found |
|
|
162
|
+
| `UNAUTHORIZED` | Invalid app secret |
|
|
163
|
+
|
|
164
|
+
## Frontend SDK vs Backend SDK
|
|
165
|
+
|
|
166
|
+
| | Frontend SDK | Backend SDK |
|
|
167
|
+
|--|-------------|-------------|
|
|
168
|
+
| Package | `@dotbots-boutique/auth-sdk` | `@dotbots-boutique/server-sdk` |
|
|
169
|
+
| Runtime | Browser | Deno / Node |
|
|
170
|
+
| Auth | User JWT (iframe) | App secret |
|
|
171
|
+
| AI calls | ✅ | ✅ |
|
|
172
|
+
| Payments | ✅ | ✅ |
|
|
173
|
+
| User context | Automatic | Manual via `orgId` |
|
|
174
|
+
| Use case | User interactions | Webhooks, jobs, server logic |
|
|
175
|
+
|
|
176
|
+
**Rule of thumb:** if there is a user actively waiting in the browser, use the frontend SDK. If the logic runs in the background or server-side, use the backend SDK.
|
|
177
|
+
|
|
178
|
+
## Security
|
|
179
|
+
|
|
180
|
+
- `DOTBOTS_APP_SECRET` is injected by the platform and must never be logged, returned in API responses or stored anywhere other than environment variables.
|
|
181
|
+
- The proxy URL is fetched dynamically at startup — never hardcode it.
|
|
182
|
+
- All communication between the backend SDK and the proxy is server-to-server. The app secret is never exposed to the browser.
|
|
183
|
+
|
|
184
|
+
## Links
|
|
185
|
+
|
|
186
|
+
- [DotBots Boutique](https://dotbots.boutique)
|
|
187
|
+
- [Developer documentation](https://docs.dotbots.boutique)
|
|
188
|
+
- [Frontend SDK](https://www.npmjs.com/package/@dotbots-boutique/auth-sdk)
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AiResponse, BackendAiRequest, DotBotsBackendConfig, DotBotsUser } from './types';
|
|
2
|
+
export declare class DotBotsBackend {
|
|
3
|
+
private appId;
|
|
4
|
+
private appSecret;
|
|
5
|
+
private apiUrl;
|
|
6
|
+
private environment;
|
|
7
|
+
private proxyUrl;
|
|
8
|
+
constructor(config: DotBotsBackendConfig);
|
|
9
|
+
initialize(): Promise<void>;
|
|
10
|
+
ai(feature: string, request: BackendAiRequest): Promise<AiResponse>;
|
|
11
|
+
aiStream(feature: string, request: BackendAiRequest, onDelta: (delta: string) => void, onDone?: (response: AiResponse) => void): Promise<void>;
|
|
12
|
+
charge(featureCode: string, options: {
|
|
13
|
+
paidBy?: 'org' | 'app';
|
|
14
|
+
orgId?: string;
|
|
15
|
+
quantity?: number;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
transactionId: string;
|
|
18
|
+
amount: bigint;
|
|
19
|
+
}>;
|
|
20
|
+
getUser(userId: string): Promise<DotBotsUser>;
|
|
21
|
+
private baseHeaders;
|
|
22
|
+
private getProxyUrl;
|
|
23
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DotBotsBackend = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
class DotBotsBackend {
|
|
6
|
+
appId;
|
|
7
|
+
appSecret;
|
|
8
|
+
apiUrl;
|
|
9
|
+
environment;
|
|
10
|
+
proxyUrl = null;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.appId = config.appId;
|
|
13
|
+
this.appSecret = config.appSecret;
|
|
14
|
+
this.apiUrl = config.apiUrl;
|
|
15
|
+
this.environment = config.environment;
|
|
16
|
+
}
|
|
17
|
+
async initialize() {
|
|
18
|
+
const response = await fetch(`${this.apiUrl}/api/proxy/config`, {
|
|
19
|
+
headers: this.baseHeaders(),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(`Failed to fetch proxy config: ${response.status}`);
|
|
23
|
+
}
|
|
24
|
+
const data = (await response.json());
|
|
25
|
+
this.proxyUrl = data.proxyUrl;
|
|
26
|
+
console.log(JSON.stringify({
|
|
27
|
+
level: 'info',
|
|
28
|
+
message: `[DotBotsBackend] Proxy config loaded — proxyUrl: ${data.proxyUrl}`,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
async ai(feature, request) {
|
|
32
|
+
const response = await fetch(`${this.getProxyUrl()}/ai/call`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: this.baseHeaders(),
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
feature,
|
|
37
|
+
messages: request.messages,
|
|
38
|
+
tools: request.tools,
|
|
39
|
+
stream: false,
|
|
40
|
+
maxTokens: request.maxTokens,
|
|
41
|
+
paidBy: request.paidBy,
|
|
42
|
+
orgId: request.orgId,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const body = (await response.json().catch(() => ({})));
|
|
47
|
+
throw new errors_1.DotBotsBackendError(body.error ?? 'AI_CALL_FAILED', response.status);
|
|
48
|
+
}
|
|
49
|
+
return (await response.json());
|
|
50
|
+
}
|
|
51
|
+
async aiStream(feature, request, onDelta, onDone) {
|
|
52
|
+
const response = await fetch(`${this.getProxyUrl()}/ai/call`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: this.baseHeaders(),
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
feature,
|
|
57
|
+
messages: request.messages,
|
|
58
|
+
tools: request.tools,
|
|
59
|
+
stream: true,
|
|
60
|
+
maxTokens: request.maxTokens,
|
|
61
|
+
paidBy: request.paidBy,
|
|
62
|
+
orgId: request.orgId,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const body = (await response.json().catch(() => ({})));
|
|
67
|
+
throw new errors_1.DotBotsBackendError(body.error ?? 'AI_CALL_FAILED', response.status);
|
|
68
|
+
}
|
|
69
|
+
const reader = response.body.getReader();
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let buffer = '';
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done)
|
|
75
|
+
break;
|
|
76
|
+
buffer += decoder.decode(value, { stream: true });
|
|
77
|
+
const lines = buffer.split('\n');
|
|
78
|
+
buffer = lines.pop() ?? '';
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (!line.startsWith('data: '))
|
|
81
|
+
continue;
|
|
82
|
+
const data = JSON.parse(line.slice(6));
|
|
83
|
+
if (data.type === 'text')
|
|
84
|
+
onDelta(data.delta);
|
|
85
|
+
else if (data.type === 'done')
|
|
86
|
+
onDone?.(data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async charge(featureCode, options) {
|
|
91
|
+
const response = await fetch(`${this.getProxyUrl()}/charge`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: this.baseHeaders(),
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
featureCode,
|
|
96
|
+
paidBy: options.paidBy,
|
|
97
|
+
orgId: options.orgId,
|
|
98
|
+
quantity: options.quantity ?? 1,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const body = (await response.json().catch(() => ({})));
|
|
103
|
+
throw new errors_1.DotBotsBackendError(body.error ?? 'CHARGE_FAILED', response.status);
|
|
104
|
+
}
|
|
105
|
+
return (await response.json());
|
|
106
|
+
}
|
|
107
|
+
async getUser(userId) {
|
|
108
|
+
const response = await fetch(`${this.getProxyUrl()}/users/${userId}`, { headers: this.baseHeaders() });
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new errors_1.DotBotsBackendError('USER_NOT_FOUND', response.status);
|
|
111
|
+
}
|
|
112
|
+
return (await response.json());
|
|
113
|
+
}
|
|
114
|
+
baseHeaders() {
|
|
115
|
+
return {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'X-App-Id': this.appId,
|
|
118
|
+
'X-App-Secret': this.appSecret,
|
|
119
|
+
'X-Environment': this.environment,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
getProxyUrl() {
|
|
123
|
+
if (!this.proxyUrl) {
|
|
124
|
+
throw new Error('DotBotsBackend not initialised — call initialize() first');
|
|
125
|
+
}
|
|
126
|
+
return this.proxyUrl;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.DotBotsBackend = DotBotsBackend;
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DotBotsBackendError = void 0;
|
|
4
|
+
class DotBotsBackendError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(code, status) {
|
|
8
|
+
super(code);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.name = 'DotBotsBackendError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.DotBotsBackendError = DotBotsBackendError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DotBotsBackend } from './client';
|
|
2
|
+
export { DotBotsBackendError } from './errors';
|
|
3
|
+
export type { AiMessage, AiResponse, AiTool, AiToolCall, AiToolParameter, BackendAiRequest, DotBotsBackendConfig, DotBotsUser, } from './types';
|
|
4
|
+
export { AppSecretsStore } from './proxy/app-secrets';
|
|
5
|
+
export { handleAiCall, handleCharge, handleGetUser, handleInternalAppSecrets, resolveAuth, } from './proxy/handlers';
|
|
6
|
+
export type { AuthResult, CryptoHelpers, DbClient, FeatureConfig, JwtPayload, UserRecord, } from './proxy/types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAuth = exports.handleInternalAppSecrets = exports.handleGetUser = exports.handleCharge = exports.handleAiCall = exports.AppSecretsStore = exports.DotBotsBackendError = exports.DotBotsBackend = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "DotBotsBackend", { enumerable: true, get: function () { return client_1.DotBotsBackend; } });
|
|
6
|
+
var errors_1 = require("./errors");
|
|
7
|
+
Object.defineProperty(exports, "DotBotsBackendError", { enumerable: true, get: function () { return errors_1.DotBotsBackendError; } });
|
|
8
|
+
var app_secrets_1 = require("./proxy/app-secrets");
|
|
9
|
+
Object.defineProperty(exports, "AppSecretsStore", { enumerable: true, get: function () { return app_secrets_1.AppSecretsStore; } });
|
|
10
|
+
var handlers_1 = require("./proxy/handlers");
|
|
11
|
+
Object.defineProperty(exports, "handleAiCall", { enumerable: true, get: function () { return handlers_1.handleAiCall; } });
|
|
12
|
+
Object.defineProperty(exports, "handleCharge", { enumerable: true, get: function () { return handlers_1.handleCharge; } });
|
|
13
|
+
Object.defineProperty(exports, "handleGetUser", { enumerable: true, get: function () { return handlers_1.handleGetUser; } });
|
|
14
|
+
Object.defineProperty(exports, "handleInternalAppSecrets", { enumerable: true, get: function () { return handlers_1.handleInternalAppSecrets; } });
|
|
15
|
+
Object.defineProperty(exports, "resolveAuth", { enumerable: true, get: function () { return handlers_1.resolveAuth; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CryptoHelpers, DbClient } from './types';
|
|
2
|
+
export declare class AppSecretsStore {
|
|
3
|
+
private secrets;
|
|
4
|
+
private db;
|
|
5
|
+
private crypto;
|
|
6
|
+
constructor(db: DbClient, crypto: CryptoHelpers);
|
|
7
|
+
loadFromDatabase(): Promise<void>;
|
|
8
|
+
upsert(appId: string, secret: string): Promise<void>;
|
|
9
|
+
validate(appId: string, secret: string): boolean;
|
|
10
|
+
validateRequest(req: Request): {
|
|
11
|
+
appId: string;
|
|
12
|
+
valid: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppSecretsStore = void 0;
|
|
4
|
+
class AppSecretsStore {
|
|
5
|
+
secrets = new Map();
|
|
6
|
+
db;
|
|
7
|
+
crypto;
|
|
8
|
+
constructor(db, crypto) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.crypto = crypto;
|
|
11
|
+
}
|
|
12
|
+
async loadFromDatabase() {
|
|
13
|
+
const result = await this.db.queryObject('SELECT app_id, secret_encrypted FROM app_secrets');
|
|
14
|
+
for (const row of result.rows) {
|
|
15
|
+
const plaintext = await this.crypto.decrypt(row.secret_encrypted);
|
|
16
|
+
this.secrets.set(row.app_id, plaintext);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async upsert(appId, secret) {
|
|
20
|
+
const encrypted = await this.crypto.encrypt(secret);
|
|
21
|
+
await this.db.queryObject(`INSERT INTO app_secrets (app_id, secret_encrypted, updated_at)
|
|
22
|
+
VALUES ($1, $2, NOW())
|
|
23
|
+
ON CONFLICT (app_id) DO UPDATE
|
|
24
|
+
SET secret_encrypted = $2, updated_at = NOW()`, [appId, encrypted]);
|
|
25
|
+
this.secrets.set(appId, secret);
|
|
26
|
+
}
|
|
27
|
+
validate(appId, secret) {
|
|
28
|
+
const stored = this.secrets.get(appId);
|
|
29
|
+
if (!stored)
|
|
30
|
+
return false;
|
|
31
|
+
return stored === secret;
|
|
32
|
+
}
|
|
33
|
+
validateRequest(req) {
|
|
34
|
+
const appId = req.headers.get('x-app-id');
|
|
35
|
+
const appSecret = req.headers.get('x-app-secret');
|
|
36
|
+
if (!appId || !appSecret)
|
|
37
|
+
return { appId: '', valid: false };
|
|
38
|
+
return { appId, valid: this.validate(appId, appSecret) };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.AppSecretsStore = AppSecretsStore;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AppSecretsStore } from './app-secrets';
|
|
2
|
+
import type { AuthResult, FeatureConfig, JwtPayload, UserRecord } from './types';
|
|
3
|
+
export declare function handleInternalAppSecrets(store: AppSecretsStore, platformSecret: string): (req: Request) => Promise<Response>;
|
|
4
|
+
export declare function resolveAuth(store: AppSecretsStore, decodeJwt: (token: string) => JwtPayload): (req: Request) => AuthResult | Response;
|
|
5
|
+
export declare function handleAiCall(store: AppSecretsStore, decodeJwt: (token: string) => JwtPayload, getFeatureConfig: (key: string) => FeatureConfig | undefined, processAiCall: (params: {
|
|
6
|
+
appId: string;
|
|
7
|
+
orgId: string | null;
|
|
8
|
+
feature: string;
|
|
9
|
+
messages: unknown[];
|
|
10
|
+
tools?: unknown[];
|
|
11
|
+
stream: boolean;
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
}) => Promise<Response>): (req: Request) => Promise<Response>;
|
|
14
|
+
export declare function handleCharge(store: AppSecretsStore, decodeJwt: (token: string) => JwtPayload, getFeatureConfig: (key: string) => FeatureConfig | undefined, processCharge: (params: {
|
|
15
|
+
appId: string;
|
|
16
|
+
orgId: string | null;
|
|
17
|
+
userId: string | null;
|
|
18
|
+
featureCode: string;
|
|
19
|
+
quantity: number;
|
|
20
|
+
}) => Promise<Response>): (req: Request) => Promise<Response>;
|
|
21
|
+
export declare function handleGetUser(store: AppSecretsStore, lookupUser: (appId: string, userId: string) => Promise<UserRecord | null>, fetchUserFromPlatform?: (appId: string, userId: string) => Promise<UserRecord | null>): (req: Request, userId: string) => Promise<Response>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleInternalAppSecrets = handleInternalAppSecrets;
|
|
4
|
+
exports.resolveAuth = resolveAuth;
|
|
5
|
+
exports.handleAiCall = handleAiCall;
|
|
6
|
+
exports.handleCharge = handleCharge;
|
|
7
|
+
exports.handleGetUser = handleGetUser;
|
|
8
|
+
function jsonResponse(body, status = 200) {
|
|
9
|
+
return new Response(JSON.stringify(body), {
|
|
10
|
+
status,
|
|
11
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function errorResponse(error, status) {
|
|
15
|
+
return jsonResponse({ error }, status);
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// POST /internal/app-secrets
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function handleInternalAppSecrets(store, platformSecret) {
|
|
21
|
+
return async (req) => {
|
|
22
|
+
if (req.headers.get('x-platform-secret') !== platformSecret) {
|
|
23
|
+
return errorResponse('UNAUTHORIZED', 401);
|
|
24
|
+
}
|
|
25
|
+
const body = (await req.json());
|
|
26
|
+
await store.upsert(body.appId, body.secret);
|
|
27
|
+
return new Response(null, { status: 200 });
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Dual auth: JWT or app secret
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function resolveAuth(store, decodeJwt) {
|
|
34
|
+
return (req) => {
|
|
35
|
+
const authHeader = req.headers.get('authorization');
|
|
36
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
37
|
+
const jwt = decodeJwt(authHeader.replace('Bearer ', ''));
|
|
38
|
+
return { appId: jwt.app_id, orgId: jwt.org_id, userId: jwt.sub };
|
|
39
|
+
}
|
|
40
|
+
if (req.headers.get('x-app-secret')) {
|
|
41
|
+
const { appId, valid } = store.validateRequest(req);
|
|
42
|
+
if (!valid)
|
|
43
|
+
return errorResponse('UNAUTHORIZED', 401);
|
|
44
|
+
return { appId, orgId: null, userId: null };
|
|
45
|
+
}
|
|
46
|
+
return errorResponse('UNAUTHORIZED', 401);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// POST /ai/call
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function handleAiCall(store, decodeJwt, getFeatureConfig, processAiCall) {
|
|
53
|
+
const authenticate = resolveAuth(store, decodeJwt);
|
|
54
|
+
return async (req) => {
|
|
55
|
+
const auth = authenticate(req);
|
|
56
|
+
if (auth instanceof Response)
|
|
57
|
+
return auth;
|
|
58
|
+
const body = (await req.json());
|
|
59
|
+
const featureConfig = getFeatureConfig(`${auth.appId}:${body.feature}`);
|
|
60
|
+
if (!featureConfig) {
|
|
61
|
+
return errorResponse('AI_FEATURE_NOT_FOUND', 400);
|
|
62
|
+
}
|
|
63
|
+
const paidBy = featureConfig.paidBy ?? 'org';
|
|
64
|
+
let orgId = auth.orgId;
|
|
65
|
+
if (!orgId && paidBy === 'org') {
|
|
66
|
+
orgId = body.orgId ?? null;
|
|
67
|
+
if (!orgId) {
|
|
68
|
+
return errorResponse('ORG_ID_REQUIRED', 400);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return processAiCall({
|
|
72
|
+
appId: auth.appId,
|
|
73
|
+
orgId,
|
|
74
|
+
feature: body.feature,
|
|
75
|
+
messages: body.messages,
|
|
76
|
+
tools: body.tools,
|
|
77
|
+
stream: body.stream ?? false,
|
|
78
|
+
maxTokens: body.maxTokens,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// POST /charge
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function handleCharge(store, decodeJwt, getFeatureConfig, processCharge) {
|
|
86
|
+
const authenticate = resolveAuth(store, decodeJwt);
|
|
87
|
+
return async (req) => {
|
|
88
|
+
const auth = authenticate(req);
|
|
89
|
+
if (auth instanceof Response)
|
|
90
|
+
return auth;
|
|
91
|
+
const body = (await req.json());
|
|
92
|
+
const featureConfig = getFeatureConfig(`${auth.appId}:${body.featureCode}`);
|
|
93
|
+
if (!featureConfig) {
|
|
94
|
+
return errorResponse('FEATURE_NOT_FOUND', 400);
|
|
95
|
+
}
|
|
96
|
+
const paidBy = featureConfig.paidBy ?? 'org';
|
|
97
|
+
let orgId = auth.orgId;
|
|
98
|
+
if (!orgId && paidBy === 'org') {
|
|
99
|
+
orgId = body.orgId ?? null;
|
|
100
|
+
if (!orgId) {
|
|
101
|
+
return errorResponse('ORG_ID_REQUIRED', 400);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return processCharge({
|
|
105
|
+
appId: auth.appId,
|
|
106
|
+
orgId,
|
|
107
|
+
userId: auth.userId,
|
|
108
|
+
featureCode: body.featureCode,
|
|
109
|
+
quantity: body.quantity ?? 1,
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// GET /users/:userId
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
function handleGetUser(store, lookupUser, fetchUserFromPlatform) {
|
|
117
|
+
return async (req, userId) => {
|
|
118
|
+
const { appId, valid } = store.validateRequest(req);
|
|
119
|
+
if (!valid) {
|
|
120
|
+
return errorResponse('UNAUTHORIZED', 401);
|
|
121
|
+
}
|
|
122
|
+
let user = await lookupUser(appId, userId);
|
|
123
|
+
if (!user && fetchUserFromPlatform) {
|
|
124
|
+
user = await fetchUserFromPlatform(appId, userId);
|
|
125
|
+
}
|
|
126
|
+
if (!user) {
|
|
127
|
+
return errorResponse('USER_NOT_FOUND', 404);
|
|
128
|
+
}
|
|
129
|
+
return jsonResponse({
|
|
130
|
+
id: user.id,
|
|
131
|
+
orgId: user.org_id,
|
|
132
|
+
roles: user.roles,
|
|
133
|
+
email: user.email,
|
|
134
|
+
name: user.name,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAuth = exports.handleInternalAppSecrets = exports.handleGetUser = exports.handleCharge = exports.handleAiCall = exports.AppSecretsStore = void 0;
|
|
4
|
+
var app_secrets_1 = require("./app-secrets");
|
|
5
|
+
Object.defineProperty(exports, "AppSecretsStore", { enumerable: true, get: function () { return app_secrets_1.AppSecretsStore; } });
|
|
6
|
+
var handlers_1 = require("./handlers");
|
|
7
|
+
Object.defineProperty(exports, "handleAiCall", { enumerable: true, get: function () { return handlers_1.handleAiCall; } });
|
|
8
|
+
Object.defineProperty(exports, "handleCharge", { enumerable: true, get: function () { return handlers_1.handleCharge; } });
|
|
9
|
+
Object.defineProperty(exports, "handleGetUser", { enumerable: true, get: function () { return handlers_1.handleGetUser; } });
|
|
10
|
+
Object.defineProperty(exports, "handleInternalAppSecrets", { enumerable: true, get: function () { return handlers_1.handleInternalAppSecrets; } });
|
|
11
|
+
Object.defineProperty(exports, "resolveAuth", { enumerable: true, get: function () { return handlers_1.resolveAuth; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface DbClient {
|
|
2
|
+
queryObject<T>(query: string, args?: unknown[]): Promise<{
|
|
3
|
+
rows: T[];
|
|
4
|
+
}>;
|
|
5
|
+
}
|
|
6
|
+
export interface CryptoHelpers {
|
|
7
|
+
encrypt(plaintext: string): Promise<string>;
|
|
8
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
export interface JwtPayload {
|
|
11
|
+
sub: string;
|
|
12
|
+
org_id: string;
|
|
13
|
+
app_id: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FeatureConfig {
|
|
16
|
+
paidBy?: 'org' | 'app';
|
|
17
|
+
}
|
|
18
|
+
export interface AuthResult {
|
|
19
|
+
appId: string | null;
|
|
20
|
+
orgId: string | null;
|
|
21
|
+
userId: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface UserRecord {
|
|
24
|
+
id: string;
|
|
25
|
+
org_id: string;
|
|
26
|
+
roles: string[];
|
|
27
|
+
email?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface DotBotsBackendConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
appSecret: string;
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
environment: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AiMessage {
|
|
8
|
+
role: 'system' | 'user' | 'assistant';
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AiToolParameter {
|
|
12
|
+
type: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
enum?: string[];
|
|
15
|
+
items?: AiToolParameter;
|
|
16
|
+
properties?: Record<string, AiToolParameter>;
|
|
17
|
+
required?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface AiTool {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters?: AiToolParameter;
|
|
23
|
+
}
|
|
24
|
+
export interface AiResponse {
|
|
25
|
+
content: string;
|
|
26
|
+
model: string;
|
|
27
|
+
provider: string;
|
|
28
|
+
usage: {
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
totalTokens: number;
|
|
32
|
+
};
|
|
33
|
+
cost: {
|
|
34
|
+
tokens: bigint;
|
|
35
|
+
eur: number;
|
|
36
|
+
};
|
|
37
|
+
transactionId: string;
|
|
38
|
+
toolCalls?: AiToolCall[];
|
|
39
|
+
}
|
|
40
|
+
export interface AiToolCall {
|
|
41
|
+
name: string;
|
|
42
|
+
arguments: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export interface BackendAiRequest {
|
|
45
|
+
messages: AiMessage[];
|
|
46
|
+
tools?: AiTool[];
|
|
47
|
+
maxTokens?: number;
|
|
48
|
+
paidBy?: 'org' | 'app';
|
|
49
|
+
orgId?: string;
|
|
50
|
+
stream?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface DotBotsUser {
|
|
53
|
+
id: string;
|
|
54
|
+
orgId: string;
|
|
55
|
+
roles: string[];
|
|
56
|
+
email?: string;
|
|
57
|
+
name?: string;
|
|
58
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dotbots-boutique/server-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DotBots Backend SDK — server-side AI calls, payments, and user lookups via the DotBots proxy",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dotbots",
|
|
16
|
+
"backend",
|
|
17
|
+
"sdk",
|
|
18
|
+
"ai",
|
|
19
|
+
"proxy"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^18.19.130",
|
|
24
|
+
"typescript": "^5.4.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|