@strand-js/openai 0.1.4 → 0.1.6

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 CHANGED
@@ -34,7 +34,53 @@ export const POST = createStrandRoute({
34
34
  })
35
35
  ```
36
36
 
37
- ### Switching from Anthropic
37
+ ## Production security checklist
38
+
39
+ Before going live, configure these options:
40
+
41
+ ```ts
42
+ createStrandHandler({
43
+ apiKey: process.env.OPENAI_API_KEY,
44
+ model: 'gpt-4o',
45
+
46
+ // 1. REQUIRED: authenticate every request
47
+ // Without this, anyone can hit your endpoint and burn your API credits.
48
+ authorize: async (request) => {
49
+ const token = request.headers.get('authorization')?.replace('Bearer ', '')
50
+ const user = await verifyToken(token)
51
+ if (!user) throw new Error('Unauthorized') // returns 401, no LLM call is made
52
+ },
53
+
54
+ // 2. RECOMMENDED: built-in rate limiting by IP
55
+ rateLimit: {
56
+ windowMs: 60_000, // 1 minute window
57
+ maxRequests: 20, // max 20 requests per IP per minute
58
+ },
59
+
60
+ // 3. RECOMMENDED: limit message size
61
+ maxMessages: 50,
62
+ maxMessageLength: 10_000,
63
+ })
64
+ ```
65
+
66
+ **Also configure on your server (outside Strand):**
67
+ - **CORS** — restrict which origins can call your endpoint
68
+ - **HTTPS** — never run in production over HTTP
69
+
70
+ ## What `authorize` is
71
+
72
+ `authorize` runs before any LLM call. If it throws, the request is rejected with a 401 and no API credits are used.
73
+
74
+ ```ts
75
+ // JWT example
76
+ authorize: async (request) => {
77
+ const token = request.headers.get('authorization')?.replace('Bearer ', '')
78
+ const user = await verifyJWT(token)
79
+ if (!user) throw new Error('Unauthorized')
80
+ }
81
+ ```
82
+
83
+ ## Switching providers
38
84
 
39
85
  Only the server changes — the React hooks are identical:
40
86
 
@@ -50,4 +96,19 @@ Only the server changes — the React hooks are identical:
50
96
  })
51
97
  ```
52
98
 
99
+ ## Config reference
100
+
101
+ | Option | Type | Description |
102
+ |---|---|---|
103
+ | `apiKey` | `string` | Your OpenAI API key |
104
+ | `model` | `string` | Model ID, e.g. `'gpt-4o'` |
105
+ | `system` | `string \| (req) => string` | System prompt |
106
+ | `tools` | `ToolDefinition[]` | Tools available to the model |
107
+ | `onToolCall` | `async (name, args, ctx) => result` | Server-side tool execution |
108
+ | `authorize` | `async (req) => void` | Throw to reject with 401 |
109
+ | `rateLimit` | `{ windowMs, maxRequests }` | Built-in IP rate limiting |
110
+ | `maxMessages` | `number` | Max messages per request (default: 100) |
111
+ | `maxMessageLength` | `number` | Max chars per message (default: 50,000) |
112
+ | `maxSteps` | `number` | Max tool call rounds (default: 10) |
113
+
53
114
  [Full documentation →](https://github.com/strand-js/strand)
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { ToolDefinition, Session } from '@strand-js/core';
1
+ import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
2
2
 
3
3
  interface StrandHandlerConfig {
4
4
  apiKey: string;
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
9
9
  request: Request;
10
10
  }) => Promise<unknown>;
11
11
  authorize?: (request: Request) => Promise<unknown> | unknown;
12
+ rateLimit?: RateLimitConfig;
12
13
  maxMessages?: number;
13
14
  maxMessageLength?: number;
14
15
  maxSteps?: number;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ToolDefinition, Session } from '@strand-js/core';
1
+ import { ToolDefinition, RateLimitConfig, Session } from '@strand-js/core';
2
2
 
3
3
  interface StrandHandlerConfig {
4
4
  apiKey: string;
@@ -9,6 +9,7 @@ interface StrandHandlerConfig {
9
9
  request: Request;
10
10
  }) => Promise<unknown>;
11
11
  authorize?: (request: Request) => Promise<unknown> | unknown;
12
+ rateLimit?: RateLimitConfig;
12
13
  maxMessages?: number;
13
14
  maxMessageLength?: number;
14
15
  maxSteps?: number;
package/dist/index.js CHANGED
@@ -60,12 +60,34 @@ data: ${JSON.stringify(data)}
60
60
 
61
61
  `);
62
62
  }
63
+ function normalizeRequest(req) {
64
+ const rawHeaders = req.headers ?? {};
65
+ return {
66
+ ...req,
67
+ headers: {
68
+ get: (name) => {
69
+ const val = rawHeaders[name.toLowerCase()];
70
+ return Array.isArray(val) ? val[0] ?? null : val ?? null;
71
+ }
72
+ }
73
+ };
74
+ }
63
75
  function createStrandHandler(config) {
64
76
  const client = new import_openai.default({ apiKey: config.apiKey });
65
77
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
66
78
  const maxSteps = config.maxSteps ?? 10;
79
+ const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
67
80
  return async (req, res) => {
81
+ const normalizedReq = normalizeRequest(req);
68
82
  const body = req.body;
83
+ if (rateLimiter) {
84
+ const ip = req.ip ?? "unknown";
85
+ const limited = rateLimiter.check(ip);
86
+ if (limited) {
87
+ res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
88
+ return;
89
+ }
90
+ }
69
91
  const validation = (0, import_core2.validateMessages)(body?.messages, {
70
92
  maxMessages: config.maxMessages,
71
93
  maxMessageLength: config.maxMessageLength
@@ -76,7 +98,7 @@ function createStrandHandler(config) {
76
98
  }
77
99
  if (config.authorize) {
78
100
  try {
79
- await config.authorize(req);
101
+ await config.authorize(normalizedReq);
80
102
  } catch (err) {
81
103
  const message = err instanceof Error ? err.message : "Unauthorized";
82
104
  res.status(401).json({ error: message });
@@ -89,7 +111,7 @@ function createStrandHandler(config) {
89
111
  emit(res, "strand:start", { sessionId: (0, import_core2.generateId)(), requestId: (0, import_core2.generateId)() });
90
112
  try {
91
113
  const messages = body.messages;
92
- const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
114
+ const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
93
115
  const conversation = [
94
116
  ...system ? [{ role: "system", content: system }] : [],
95
117
  ...messages.map((m) => ({ role: m.role, content: m.content }))
@@ -154,7 +176,7 @@ function createStrandHandler(config) {
154
176
  completedTools.map(async (block) => {
155
177
  emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
156
178
  try {
157
- const result = await config.onToolCall?.(block.name, block.input, { request: req });
179
+ const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
158
180
  emit(res, "strand:tool-result", { toolCallId: block.id, result });
159
181
  return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
160
182
  } catch (err) {
@@ -192,7 +214,18 @@ function createStrandRoute(config) {
192
214
  const client = new import_openai2.default({ apiKey: config.apiKey });
193
215
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
194
216
  const maxSteps = config.maxSteps ?? 10;
217
+ const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
195
218
  return async (req) => {
219
+ if (rateLimiter) {
220
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
221
+ const limited = rateLimiter.check(ip);
222
+ if (limited) {
223
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
224
+ status: 429,
225
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
226
+ });
227
+ }
228
+ }
196
229
  const body = await req.json();
197
230
  const validation = (0, import_core3.validateMessages)(body?.messages, {
198
231
  maxMessages: config.maxMessages,
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/handler.ts
2
2
  import OpenAI from "openai";
3
- import { generateId, validateMessages } from "@strand-js/core";
3
+ import { generateId, validateMessages, RateLimiter } from "@strand-js/core";
4
4
 
5
5
  // src/format.ts
6
6
  import { toolToJsonSchema } from "@strand-js/core";
@@ -23,12 +23,34 @@ data: ${JSON.stringify(data)}
23
23
 
24
24
  `);
25
25
  }
26
+ function normalizeRequest(req) {
27
+ const rawHeaders = req.headers ?? {};
28
+ return {
29
+ ...req,
30
+ headers: {
31
+ get: (name) => {
32
+ const val = rawHeaders[name.toLowerCase()];
33
+ return Array.isArray(val) ? val[0] ?? null : val ?? null;
34
+ }
35
+ }
36
+ };
37
+ }
26
38
  function createStrandHandler(config) {
27
39
  const client = new OpenAI({ apiKey: config.apiKey });
28
40
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
29
41
  const maxSteps = config.maxSteps ?? 10;
42
+ const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
30
43
  return async (req, res) => {
44
+ const normalizedReq = normalizeRequest(req);
31
45
  const body = req.body;
46
+ if (rateLimiter) {
47
+ const ip = req.ip ?? "unknown";
48
+ const limited = rateLimiter.check(ip);
49
+ if (limited) {
50
+ res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
51
+ return;
52
+ }
53
+ }
32
54
  const validation = validateMessages(body?.messages, {
33
55
  maxMessages: config.maxMessages,
34
56
  maxMessageLength: config.maxMessageLength
@@ -39,7 +61,7 @@ function createStrandHandler(config) {
39
61
  }
40
62
  if (config.authorize) {
41
63
  try {
42
- await config.authorize(req);
64
+ await config.authorize(normalizedReq);
43
65
  } catch (err) {
44
66
  const message = err instanceof Error ? err.message : "Unauthorized";
45
67
  res.status(401).json({ error: message });
@@ -52,7 +74,7 @@ function createStrandHandler(config) {
52
74
  emit(res, "strand:start", { sessionId: generateId(), requestId: generateId() });
53
75
  try {
54
76
  const messages = body.messages;
55
- const system = typeof config.system === "function" ? await config.system(req) : config.system ?? "";
77
+ const system = typeof config.system === "function" ? await config.system(normalizedReq) : config.system ?? "";
56
78
  const conversation = [
57
79
  ...system ? [{ role: "system", content: system }] : [],
58
80
  ...messages.map((m) => ({ role: m.role, content: m.content }))
@@ -117,7 +139,7 @@ function createStrandHandler(config) {
117
139
  completedTools.map(async (block) => {
118
140
  emit(res, "strand:tool-input-done", { toolCallId: block.id, input: block.input });
119
141
  try {
120
- const result = await config.onToolCall?.(block.name, block.input, { request: req });
142
+ const result = await config.onToolCall?.(block.name, block.input, { request: normalizedReq });
121
143
  emit(res, "strand:tool-result", { toolCallId: block.id, result });
122
144
  return { role: "tool", tool_call_id: block.id, content: JSON.stringify(result ?? null) };
123
145
  } catch (err) {
@@ -144,7 +166,7 @@ function createStrandHandler(config) {
144
166
 
145
167
  // src/route.ts
146
168
  import OpenAI2 from "openai";
147
- import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
169
+ import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
148
170
  function sseChunk(eventType, data) {
149
171
  return new TextEncoder().encode(`event: ${eventType}
150
172
  data: ${JSON.stringify(data)}
@@ -155,7 +177,18 @@ function createStrandRoute(config) {
155
177
  const client = new OpenAI2({ apiKey: config.apiKey });
156
178
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
157
179
  const maxSteps = config.maxSteps ?? 10;
180
+ const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
158
181
  return async (req) => {
182
+ if (rateLimiter) {
183
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
184
+ const limited = rateLimiter.check(ip);
185
+ if (limited) {
186
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
187
+ status: 429,
188
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
189
+ });
190
+ }
191
+ }
159
192
  const body = await req.json();
160
193
  const validation = validateMessages2(body?.messages, {
161
194
  maxMessages: config.maxMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strand-js/openai",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "description": "OpenAI provider adapter for Strand",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "openai": "^4.77.0",
21
- "@strand-js/core": "0.1.4"
21
+ "@strand-js/core": "0.1.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/express": "^5.0.0",