backprompter-server-sdk 0.1.1 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +118 -0
  2. package/index.js +271 -1
  3. package/package.json +23 -23
package/README.md CHANGED
@@ -28,6 +28,124 @@ const result = await bp.run({
28
28
  console.log(result.response);
29
29
  ```
30
30
 
31
+ ## Attachments
32
+
33
+ `run()` takes `fileInputs` and `urlInputs`, and which key you use inside
34
+ `fileInputs` decides how an attachment is sent:
35
+
36
+ | Key | Sent as |
37
+ |---|---|
38
+ | `url`, `dataUrl`, `base64` | image |
39
+ | `file_url`, `file_data` | document |
40
+
41
+ The backend infers the real type where it can — an explicit `mimeType`, the
42
+ data-URL prefix, the file extension, or the leading bytes of base64 — and falls
43
+ back to the key only when the type cannot be determined.
44
+
45
+ ```js
46
+ await bp.run({
47
+ agentId: 'analyst',
48
+ userPrompt: 'What are the Q3 totals?',
49
+ fileInputs: [
50
+ { file_url: 'https://example.com/q3.pdf', filename: 'q3.pdf' }, // document
51
+ { url: 'https://example.com/chart.png' } // image
52
+ ]
53
+ });
54
+ ```
55
+
56
+ Document support is per model: OpenAI models accept spreadsheets, while
57
+ Anthropic accepts PDF only and rejects other types outright. Pick a model that
58
+ handles what you send.
59
+
60
+ ## Use your own login
61
+
62
+ Your app can keep its own login. Backprompter never has to see a password.
63
+
64
+ Your backend exchanges your user id for a session token. Your frontend signs in
65
+ with that token. The frontend then reaches every browser SDK function, including
66
+ `saveData`, `search`, file handling and voice.
67
+
68
+ ```js
69
+ // your backend, behind your own session check
70
+ app.post('/api/bp-token', requireMyAuth, async (req, res) => {
71
+ const { customToken } = await bp.createUserToken({
72
+ externalId: req.user.id, // your own user id
73
+ email: req.user.email // a label for your dashboard
74
+ });
75
+ res.json({ customToken });
76
+ });
77
+ ```
78
+
79
+ ```js
80
+ // your frontend, after your own login succeeds
81
+ const { customToken } = await fetch('/api/bp-token').then((r) => r.json());
82
+ await Backprompter.signInWithCustomToken(customToken);
83
+ ```
84
+
85
+ Backprompter derives a storage id from your tenant id and your external id. The
86
+ same external id always returns to the same data. You cannot choose the id.
87
+
88
+ Keep the API key on your server. Only the token reaches the browser. A token is
89
+ valid for one user of one app.
90
+
91
+ ## End-user data from your backend
92
+
93
+ You can also skip the browser session and do everything from your server. Call
94
+ `forUser()` with your own user id.
95
+
96
+ ```js
97
+ const user = bp.forUser('u_8821');
98
+
99
+ await user.saveData('preferences', { theme: 'dark' });
100
+ const prefs = await user.getData('preferences');
101
+
102
+ await user.saveConversation(conversationId, messages, { title: 'Support' });
103
+ const list = await user.listConversations();
104
+
105
+ await user.storeArtifacts([{ name: 'notes.md', content: text }]);
106
+ const hits = await user.search('last quarter revenue', { k: 5 });
107
+ ```
108
+
109
+ Every call reads and writes that person's data only. The API key decides the app.
110
+ The external id decides the person.
111
+
112
+ The handle covers stored data. Agent calls stay on `bp.run()`, because a run is
113
+ not bound to a person. Pass what the agent should know about them in
114
+ `profileString`.
115
+
116
+ ```js
117
+ const profile = await user.getData('profile');
118
+ const result = await bp.run({
119
+ agentId: 'analyst',
120
+ userPrompt: 'What changed?',
121
+ profileString: JSON.stringify(profile.data)
122
+ });
123
+ ```
124
+
125
+ ### API key scopes
126
+
127
+ New API keys carry three scopes: `run:public`, `enduser:data`, and
128
+ `identity:exchange`. Keys generated before these features existed carry only
129
+ `run:public`. To use `forUser()` or `createUserToken()`, generate a new key.
130
+
131
+ ## Payments
132
+
133
+ An API key never triggers the Backprompter payment gate. Your backend is your
134
+ own integration, so you decide what a person may do.
135
+
136
+ Read the entitlement yourself when you want to enforce payment:
137
+
138
+ ```js
139
+ const { active, paymentRequired } = await bp.forUser('u_8821').getEntitlements();
140
+ if (paymentRequired && !active) return res.status(402).json({ error: 'Subscribe first' });
141
+ ```
142
+
143
+ The gate applies only to end users signed in with a Backprompter token. That
144
+ includes users who signed in with a custom token from `createUserToken()`.
145
+
146
+ If you collect money outside Backprompter, leave the Backprompter payment
147
+ settings empty. No gate then applies to anyone.
148
+
31
149
  ## Notes
32
150
 
33
151
  - Requires Node.js 18+ (uses built-in fetch).
package/index.js CHANGED
@@ -65,7 +65,12 @@ export default class BackprompterServer {
65
65
  baseText,
66
66
  ragCharBudget,
67
67
  tenantId,
68
- apiKey
68
+ apiKey,
69
+ // Streaming: the Promise still resolves with the complete result; these
70
+ // callbacks see the answer as it is written. See _streamRun.
71
+ stream,
72
+ onDelta,
73
+ onStatus
69
74
  } = {}) {
70
75
  const resolvedApiKey = String(apiKey || this._apiKey || '').trim();
71
76
  if (!resolvedApiKey) throw new Error('BackprompterServer: apiKey is required.');
@@ -88,12 +93,162 @@ export default class BackprompterServer {
88
93
  if (baseText !== undefined) body.base_text = baseText;
89
94
  if (ragCharBudget !== undefined) body.rag_char_budget = ragCharBudget;
90
95
 
96
+ if (stream) return this._streamRun(resolvedApiKey, body, { onDelta, onStatus });
91
97
  return this._apiFetchWithApiKey('/public/run', resolvedApiKey, {
92
98
  method: 'POST',
93
99
  body: JSON.stringify(body)
94
100
  });
95
101
  }
96
102
 
103
+ // POST /public/run with "stream": true and read the server-sent events.
104
+ // Resolves with the same object as a non-streamed run (the `done` event).
105
+ // onDelta(text, fullTextSoFar) — each piece of the answer
106
+ // onStatus(event) — { type: 'meta', agentId, artifactType, streaming }
107
+ // { type: 'status', stage: 'routing' | 'retrieving' | 'tool', name? }
108
+ // { type: 'reset' } (discard the text so far)
109
+ // A plain JSON reply (a refusal, or a backend without streaming) is handled
110
+ // like an ordinary run.
111
+ async _streamRun(apiKey, body, { onDelta, onStatus } = {}) {
112
+ assertFetchAvailable();
113
+ const response = await fetch(`${this._backendUrl}/public/run`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'x-bp-api-key': apiKey },
116
+ body: JSON.stringify({ ...body, stream: true })
117
+ });
118
+ const type = response.headers.get('content-type') || '';
119
+ if (!type.startsWith('text/event-stream') || !response.body) {
120
+ const payload = await response.json().catch(() => ({}));
121
+ if (!response.ok) throw new Error(getErrorMessage(response.status, payload));
122
+ return payload;
123
+ }
124
+
125
+ const call = (fn, ...args) => {
126
+ if (typeof fn !== 'function') return;
127
+ try { fn(...args); } catch (e) { console.error('BackprompterServer stream callback error:', e); }
128
+ };
129
+ let text = '';
130
+ let result = null;
131
+ let failure = null;
132
+ const handle = (name, raw) => {
133
+ let data;
134
+ try { data = JSON.parse(raw); } catch (_) { return; }
135
+ if (name === 'delta') {
136
+ const piece = String((data && data.text) || '');
137
+ if (!piece) return;
138
+ text += piece;
139
+ call(onDelta, piece, text);
140
+ } else if (name === 'reset') {
141
+ text = '';
142
+ call(onStatus, { type: 'reset' });
143
+ } else if (name === 'meta' || name === 'status') {
144
+ call(onStatus, { type: name, ...data });
145
+ } else if (name === 'done') {
146
+ result = data;
147
+ } else if (name === 'error') {
148
+ failure = new Error(getErrorMessage(data && data.status, data));
149
+ }
150
+ };
151
+
152
+ let buffer = '';
153
+ let eventName = '';
154
+ let dataLines = [];
155
+ const line = (l) => {
156
+ if (l.endsWith('\r')) l = l.slice(0, -1);
157
+ if (l === '') {
158
+ if (dataLines.length) handle(eventName || 'message', dataLines.join('\n'));
159
+ eventName = '';
160
+ dataLines = [];
161
+ } else if (l.startsWith('event:')) {
162
+ eventName = l.slice(6).trim();
163
+ } else if (l.startsWith('data:')) {
164
+ dataLines.push(l.slice(5).replace(/^ /, ''));
165
+ }
166
+ };
167
+ const decoder = new TextDecoder();
168
+ for await (const chunk of response.body) {
169
+ buffer += decoder.decode(chunk, { stream: true });
170
+ let nl;
171
+ while ((nl = buffer.indexOf('\n')) !== -1) {
172
+ line(buffer.slice(0, nl));
173
+ buffer = buffer.slice(nl + 1);
174
+ }
175
+ }
176
+ buffer += decoder.decode();
177
+ if (buffer) line(buffer);
178
+ line('');
179
+
180
+ if (failure) throw failure;
181
+ if (!result) throw new Error('BackprompterServer: the reply stream ended before the answer was complete.');
182
+ return result;
183
+ }
184
+
185
+ // ── Identity ────────────────────────────────────────────────────────────
186
+
187
+ /**
188
+ * Exchange your own user id for a Backprompter session token.
189
+ *
190
+ * Call this from your backend after your own login succeeds. Send the token
191
+ * to your frontend and pass it to Backprompter.signInWithCustomToken(token).
192
+ * Your frontend then reaches every browser SDK function. Your API key stays
193
+ * on your server.
194
+ *
195
+ * The API key must carry the identity:exchange scope.
196
+ *
197
+ * @param {object} opts
198
+ * @param {string} opts.externalId — your own user id, required
199
+ * @param {string} [opts.email] — label only, shown in your dashboard
200
+ * @param {string} [opts.displayName]
201
+ * @param {object} [opts.claims] — extra claims. Reserved names are dropped.
202
+ * @returns {Promise<{ customToken: string, uid: string, tenantId: string }>}
203
+ */
204
+ async createUserToken({ externalId, email, displayName, claims, tenantId, apiKey } = {}) {
205
+ const resolvedExternalId = String(externalId || '').trim();
206
+ if (!resolvedExternalId) throw new Error('BackprompterServer: externalId is required.');
207
+
208
+ const body = { externalId: resolvedExternalId };
209
+ if (email) body.email = String(email).trim();
210
+ if (displayName) body.displayName = String(displayName).trim();
211
+ if (claims && typeof claims === 'object') body.claims = claims;
212
+
213
+ return this._endUserFetch('exchange', {
214
+ method: 'POST',
215
+ body: JSON.stringify(body)
216
+ }, { tenantId, apiKey });
217
+ }
218
+
219
+ // ── End-user data ───────────────────────────────────────────────────────
220
+
221
+ /**
222
+ * A handle that acts for one of your users. Every call on it reads and writes
223
+ * that person's own data, and nobody else's.
224
+ *
225
+ * @param {string} externalId — your own user id
226
+ * @returns {BackprompterUser}
227
+ */
228
+ forUser(externalId) {
229
+ const resolved = String(externalId || '').trim();
230
+ if (!resolved) throw new Error('BackprompterServer: externalId is required.');
231
+ return new BackprompterUser(this, resolved);
232
+ }
233
+
234
+ _requireTenantId(tenantId) {
235
+ const resolved = String(tenantId || this._tenantId || '').trim();
236
+ if (!resolved) throw new Error('BackprompterServer: tenantId is required.');
237
+ return resolved;
238
+ }
239
+
240
+ async _endUserFetch(subPath, options = {}, { tenantId, apiKey, externalId } = {}) {
241
+ const resolvedApiKey = String(apiKey || this._apiKey || '').trim();
242
+ if (!resolvedApiKey) throw new Error('BackprompterServer: apiKey is required.');
243
+ const resolvedTenantId = this._requireTenantId(tenantId);
244
+
245
+ const headers = { ...(options.headers || {}) };
246
+ if (externalId) headers['x-bp-end-user'] = String(externalId);
247
+
248
+ const path = `/api/app/${encodeURIComponent(resolvedTenantId)}/enduser/${subPath}`;
249
+ return this._apiFetchWithApiKey(path, resolvedApiKey, { ...options, headers });
250
+ }
251
+
97
252
  async _apiFetchWithApiKey(path, apiKey, options = {}) {
98
253
  assertFetchAvailable();
99
254
 
@@ -117,3 +272,118 @@ export default class BackprompterServer {
117
272
  return payload;
118
273
  }
119
274
  }
275
+
276
+ /**
277
+ * Acts for one of your users. Create it with backprompterServer.forUser(id).
278
+ *
279
+ * The API key decides which app the calls reach. The external id decides which
280
+ * person inside that app. Backprompter derives a stable storage id from the two,
281
+ * so the same external id always returns to the same data.
282
+ */
283
+ export class BackprompterUser {
284
+ constructor(server, externalId) {
285
+ this._server = server;
286
+ this._externalId = externalId;
287
+ }
288
+
289
+ get externalId() {
290
+ return this._externalId;
291
+ }
292
+
293
+ _fetch(subPath, options = {}) {
294
+ return this._server._endUserFetch(subPath, options, { externalId: this._externalId });
295
+ }
296
+
297
+ /** Mint a browser session token for this user. */
298
+ createToken(opts = {}) {
299
+ return this._server.createUserToken({ ...opts, externalId: this._externalId });
300
+ }
301
+
302
+ // ── Custom data ───────────────────────────────────────────────────────
303
+
304
+ saveData(docId, data) {
305
+ return this._fetch(`data/${encodeURIComponent(docId)}`, {
306
+ method: 'POST',
307
+ body: JSON.stringify({ data })
308
+ });
309
+ }
310
+
311
+ getData(docId) {
312
+ return this._fetch(`data/${encodeURIComponent(docId)}`);
313
+ }
314
+
315
+ deleteData(docId) {
316
+ return this._fetch(`data/${encodeURIComponent(docId)}`, { method: 'DELETE' });
317
+ }
318
+
319
+ // ── Conversations ─────────────────────────────────────────────────────
320
+
321
+ saveConversation(conversationId, messages, metadata = {}) {
322
+ return this._fetch(`conversations/${encodeURIComponent(conversationId)}`, {
323
+ method: 'POST',
324
+ body: JSON.stringify({ messages, metadata })
325
+ });
326
+ }
327
+
328
+ listConversations() {
329
+ return this._fetch('conversations');
330
+ }
331
+
332
+ getConversation(conversationId) {
333
+ return this._fetch(`conversations/${encodeURIComponent(conversationId)}`);
334
+ }
335
+
336
+ deleteConversation(conversationId) {
337
+ return this._fetch(`conversations/${encodeURIComponent(conversationId)}`, { method: 'DELETE' });
338
+ }
339
+
340
+ // ── Artifacts ─────────────────────────────────────────────────────────
341
+
342
+ storeArtifacts(files) {
343
+ return this._fetch('artifacts', {
344
+ method: 'POST',
345
+ body: JSON.stringify({ files })
346
+ });
347
+ }
348
+
349
+ listArtifacts() {
350
+ return this._fetch('artifacts');
351
+ }
352
+
353
+ getArtifact(docId) {
354
+ return this._fetch(`artifacts/${encodeURIComponent(docId)}`);
355
+ }
356
+
357
+ deleteArtifact(docId) {
358
+ return this._fetch(`artifacts/${encodeURIComponent(docId)}`, { method: 'DELETE' });
359
+ }
360
+
361
+ // ── Search ────────────────────────────────────────────────────────────
362
+
363
+ search(query, { searchIn = ['conversations', 'artifacts'], k = 5 } = {}) {
364
+ return this._fetch('search', {
365
+ method: 'POST',
366
+ body: JSON.stringify({ query, searchIn, k })
367
+ });
368
+ }
369
+
370
+ // ── Payments ──────────────────────────────────────────────────────────
371
+
372
+ getEntitlements() {
373
+ return this._fetch('entitlements');
374
+ }
375
+
376
+ createCheckoutSession({ successUrl, cancelUrl } = {}) {
377
+ return this._fetch('checkout-session', {
378
+ method: 'POST',
379
+ body: JSON.stringify({ successUrl, cancelUrl })
380
+ });
381
+ }
382
+
383
+ createBillingPortalSession({ returnUrl } = {}) {
384
+ return this._fetch('billing-portal', {
385
+ method: 'POST',
386
+ body: JSON.stringify({ returnUrl })
387
+ });
388
+ }
389
+ }
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "backprompter-server-sdk",
3
- "version": "0.1.1",
4
- "description": "Backprompter server-side SDK for Node.js backends",
5
- "type": "module",
6
- "main": "index.js",
7
- "exports": {
8
- ".": "./index.js"
9
- },
10
- "publishConfig": {
11
- "access": "public"
12
- },
13
- "engines": {
14
- "node": ">=18"
15
- },
16
- "keywords": [
17
- "backprompter",
18
- "sdk",
19
- "server",
20
- "node"
21
- ],
22
- "license": "MIT"
23
- }
1
+ {
2
+ "name": "backprompter-server-sdk",
3
+ "version": "0.1.3",
4
+ "description": "Backprompter server-side SDK for Node.js backends",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "backprompter",
18
+ "sdk",
19
+ "server",
20
+ "node"
21
+ ],
22
+ "license": "MIT"
23
+ }