@strand-js/openai 0.1.3 → 0.1.5

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
@@ -64,8 +64,17 @@ function createStrandHandler(config) {
64
64
  const client = new import_openai.default({ apiKey: config.apiKey });
65
65
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
66
66
  const maxSteps = config.maxSteps ?? 10;
67
+ const rateLimiter = config.rateLimit ? new import_core2.RateLimiter(config.rateLimit) : null;
67
68
  return async (req, res) => {
68
69
  const body = req.body;
70
+ if (rateLimiter) {
71
+ const ip = req.ip ?? "unknown";
72
+ const limited = rateLimiter.check(ip);
73
+ if (limited) {
74
+ res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
75
+ return;
76
+ }
77
+ }
69
78
  const validation = (0, import_core2.validateMessages)(body?.messages, {
70
79
  maxMessages: config.maxMessages,
71
80
  maxMessageLength: config.maxMessageLength
@@ -192,7 +201,18 @@ function createStrandRoute(config) {
192
201
  const client = new import_openai2.default({ apiKey: config.apiKey });
193
202
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
194
203
  const maxSteps = config.maxSteps ?? 10;
204
+ const rateLimiter = config.rateLimit ? new import_core3.RateLimiter(config.rateLimit) : null;
195
205
  return async (req) => {
206
+ if (rateLimiter) {
207
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
208
+ const limited = rateLimiter.check(ip);
209
+ if (limited) {
210
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
211
+ status: 429,
212
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
213
+ });
214
+ }
215
+ }
196
216
  const body = await req.json();
197
217
  const validation = (0, import_core3.validateMessages)(body?.messages, {
198
218
  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";
@@ -27,8 +27,17 @@ function createStrandHandler(config) {
27
27
  const client = new OpenAI({ apiKey: config.apiKey });
28
28
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
29
29
  const maxSteps = config.maxSteps ?? 10;
30
+ const rateLimiter = config.rateLimit ? new RateLimiter(config.rateLimit) : null;
30
31
  return async (req, res) => {
31
32
  const body = req.body;
33
+ if (rateLimiter) {
34
+ const ip = req.ip ?? "unknown";
35
+ const limited = rateLimiter.check(ip);
36
+ if (limited) {
37
+ res.status(429).json({ error: "Too many requests", retryAfter: limited.retryAfter });
38
+ return;
39
+ }
40
+ }
32
41
  const validation = validateMessages(body?.messages, {
33
42
  maxMessages: config.maxMessages,
34
43
  maxMessageLength: config.maxMessageLength
@@ -144,7 +153,7 @@ function createStrandHandler(config) {
144
153
 
145
154
  // src/route.ts
146
155
  import OpenAI2 from "openai";
147
- import { generateId as generateId2, validateMessages as validateMessages2 } from "@strand-js/core";
156
+ import { generateId as generateId2, validateMessages as validateMessages2, RateLimiter as RateLimiter2 } from "@strand-js/core";
148
157
  function sseChunk(eventType, data) {
149
158
  return new TextEncoder().encode(`event: ${eventType}
150
159
  data: ${JSON.stringify(data)}
@@ -155,7 +164,18 @@ function createStrandRoute(config) {
155
164
  const client = new OpenAI2({ apiKey: config.apiKey });
156
165
  const openAITools = (config.tools ?? []).map(toolToOpenAITool);
157
166
  const maxSteps = config.maxSteps ?? 10;
167
+ const rateLimiter = config.rateLimit ? new RateLimiter2(config.rateLimit) : null;
158
168
  return async (req) => {
169
+ if (rateLimiter) {
170
+ const ip = req.headers.get("x-forwarded-for") ?? req.headers.get("x-real-ip") ?? "unknown";
171
+ const limited = rateLimiter.check(ip);
172
+ if (limited) {
173
+ return new Response(JSON.stringify({ error: "Too many requests", retryAfter: limited.retryAfter }), {
174
+ status: 429,
175
+ headers: { "Content-Type": "application/json", "Retry-After": String(limited.retryAfter) }
176
+ });
177
+ }
178
+ }
159
179
  const body = await req.json();
160
180
  const validation = validateMessages2(body?.messages, {
161
181
  maxMessages: config.maxMessages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strand-js/openai",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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.3"
21
+ "@strand-js/core": "0.1.5"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/express": "^5.0.0",