openkbs 0.0.64 → 0.0.66

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 (41) hide show
  1. package/elastic/README.md +1150 -0
  2. package/elastic/functions.md +328 -0
  3. package/elastic/postgres.md +287 -0
  4. package/elastic/pulse.md +386 -0
  5. package/elastic/storage.md +291 -0
  6. package/package.json +1 -1
  7. package/src/actions.js +76 -92
  8. package/src/index.js +23 -10
  9. package/src/utils.js +8 -4
  10. package/templates/.claude/skills/openkbs/SKILL.md +184 -0
  11. package/templates/.claude/skills/openkbs/metadata.json +1 -0
  12. package/templates/.claude/skills/openkbs/reference/backend-sdk.md +428 -0
  13. package/templates/.claude/skills/openkbs/reference/commands.md +370 -0
  14. package/templates/.claude/skills/openkbs/reference/elastic-services.md +327 -0
  15. package/templates/.claude/skills/openkbs/reference/frontend-sdk.md +299 -0
  16. package/version.json +3 -3
  17. package/templates/.openkbs/knowledge/metadata.json +0 -3
  18. package/templates/CLAUDE.md +0 -655
  19. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/icon.png +0 -0
  20. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/instructions.txt +0 -0
  21. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/settings.json +0 -0
  22. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/run_job.js +0 -0
  23. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -0
  24. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/actions.js +0 -0
  25. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/handler.js +0 -0
  26. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.js +0 -0
  27. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.json +0 -0
  28. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.js +0 -0
  29. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.json +0 -0
  30. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.js +0 -0
  31. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.json +0 -0
  32. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/README.md +0 -0
  33. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/instructions.txt +0 -0
  34. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/settings.json +0 -0
  35. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/actions.js +0 -0
  36. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.js +0 -0
  37. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.json +0 -0
  38. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.js +0 -0
  39. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.json +0 -0
  40. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.js +0 -0
  41. /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.json +0 -0
@@ -0,0 +1,386 @@
1
+ # Tutorial 14: Real-time with Pulse
2
+
3
+ Add real-time features to your app with Pulse WebSocket messaging. Build live updates, chat, presence tracking, and collaborative features.
4
+
5
+ ## Enable Pulse
6
+
7
+ ```bash
8
+ openkbs pulse enable
9
+ ```
10
+
11
+ ## How It Works
12
+
13
+ ```
14
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
15
+ │ Browser 1 │ │ Pulse │ │ Browser 2 │
16
+ │ │────▶│ Server │────▶│ │
17
+ │ Subscribe │ │ │ │ Receive │
18
+ │ to 'posts' │ │ │ │ message │
19
+ └──────────────┘ └──────────────┘ └──────────────┘
20
+
21
+ │ Publish
22
+ ┌──────────────┐
23
+ │ Lambda │
24
+ │ Function │
25
+ └──────────────┘
26
+ ```
27
+
28
+ 1. Clients connect via WebSocket and subscribe to channels
29
+ 2. Your Lambda publishes events to channels
30
+ 3. All subscribers receive the message instantly
31
+
32
+ ## Client SDK (Browser)
33
+
34
+ ### Install
35
+
36
+ ```html
37
+ <script src="https://unpkg.com/openkbs-pulse@2.0.1/pulse.js"></script>
38
+ ```
39
+
40
+ Or with npm:
41
+ ```bash
42
+ npm install openkbs-pulse
43
+ ```
44
+
45
+ ### Connect
46
+
47
+ ```javascript
48
+ // Get token from your API (see Server SDK below)
49
+ const { token, endpoint, kbId } = await fetch('/auth', {
50
+ method: 'POST',
51
+ body: JSON.stringify({ action: 'getPulseToken', userId: 'user123' })
52
+ }).then(r => r.json());
53
+
54
+ // Connect to Pulse
55
+ const realtime = new Pulse.Realtime({
56
+ kbId,
57
+ token,
58
+ endpoint,
59
+ clientId: 'user123'
60
+ });
61
+
62
+ // Connection events
63
+ realtime.connection.on('connected', () => console.log('Connected!'));
64
+ realtime.connection.on('disconnected', () => console.log('Disconnected'));
65
+ ```
66
+
67
+ ### Subscribe to Channel
68
+
69
+ ```javascript
70
+ const channel = realtime.channels.get('posts');
71
+
72
+ // Subscribe to specific event
73
+ channel.subscribe('new_post', (message) => {
74
+ console.log('New post:', message.data);
75
+ });
76
+
77
+ // Subscribe to all events
78
+ channel.subscribe((message) => {
79
+ console.log('Event:', message.name, message.data);
80
+ });
81
+ ```
82
+
83
+ ### Presence (Who's Online)
84
+
85
+ ```javascript
86
+ const channel = realtime.channels.get('posts');
87
+
88
+ // Enter presence with data
89
+ channel.presence.enter({ name: 'Alice', avatar: '/alice.jpg' });
90
+
91
+ // Get current members
92
+ channel.presence.get((members) => {
93
+ console.log('Online:', members.length);
94
+ members.forEach(m => console.log(m.data.name));
95
+ });
96
+
97
+ // Subscribe to presence changes
98
+ channel.presence.subscribe((members) => {
99
+ console.log('Members updated:', members);
100
+ });
101
+
102
+ // Specific events
103
+ channel.presence.subscribe('enter', (member) => {
104
+ console.log(`${member.data.name} joined`);
105
+ });
106
+
107
+ channel.presence.subscribe('leave', (member) => {
108
+ console.log(`${member.data.name} left`);
109
+ });
110
+
111
+ // Leave when done
112
+ channel.presence.leave();
113
+ ```
114
+
115
+ ### Disconnect
116
+
117
+ ```javascript
118
+ realtime.close();
119
+ ```
120
+
121
+ ## Server SDK (Lambda)
122
+
123
+ ### Generate Token
124
+
125
+ Your Lambda must generate tokens for clients:
126
+
127
+ ```javascript
128
+ import pulse from 'openkbs-pulse/server';
129
+
130
+ export async function handler(event) {
131
+ const { action, userId } = JSON.parse(event.body || '{}');
132
+ const kbId = process.env.OPENKBS_KB_ID;
133
+ const apiKey = process.env.OPENKBS_API_KEY;
134
+
135
+ if (action === 'getPulseToken') {
136
+ const tokenData = await pulse.getToken(kbId, apiKey, userId);
137
+ return {
138
+ statusCode: 200,
139
+ body: JSON.stringify({
140
+ token: tokenData.token,
141
+ endpoint: tokenData.endpoint,
142
+ kbId
143
+ })
144
+ };
145
+ }
146
+ }
147
+ ```
148
+
149
+ ### Publish Events
150
+
151
+ ```javascript
152
+ import pulse from 'openkbs-pulse/server';
153
+
154
+ export async function handler(event) {
155
+ const { action, ...data } = JSON.parse(event.body || '{}');
156
+ const kbId = process.env.OPENKBS_KB_ID;
157
+ const apiKey = process.env.OPENKBS_API_KEY;
158
+
159
+ if (action === 'createPost') {
160
+ // Save to database...
161
+ const post = { id: 1, title: data.title, content: data.content };
162
+
163
+ // Publish to all subscribers
164
+ await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
165
+
166
+ return { statusCode: 200, body: JSON.stringify({ post }) };
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### Get Presence
172
+
173
+ ```javascript
174
+ const presence = await pulse.presence('posts', { kbId, apiKey });
175
+ console.log('Online count:', presence.count);
176
+ console.log('Members:', presence.members);
177
+ ```
178
+
179
+ ## Complete Example
180
+
181
+ ### Backend (posts/index.mjs)
182
+
183
+ ```javascript
184
+ import pg from 'pg';
185
+ import pulse from 'openkbs-pulse/server';
186
+
187
+ const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
188
+ let connected = false;
189
+
190
+ export async function handler(event) {
191
+ if (!connected) {
192
+ await db.connect();
193
+ connected = true;
194
+ await db.query(`
195
+ CREATE TABLE IF NOT EXISTS posts (
196
+ id SERIAL PRIMARY KEY,
197
+ user_name TEXT,
198
+ content TEXT,
199
+ created_at TIMESTAMP DEFAULT NOW()
200
+ )
201
+ `);
202
+ }
203
+
204
+ const headers = {
205
+ 'Content-Type': 'application/json',
206
+ 'Access-Control-Allow-Origin': '*',
207
+ 'Access-Control-Allow-Headers': 'Content-Type'
208
+ };
209
+
210
+ if (event.requestContext?.http?.method === 'OPTIONS') {
211
+ return { statusCode: 200, headers, body: '' };
212
+ }
213
+
214
+ const { action, ...data } = JSON.parse(event.body || '{}');
215
+ const kbId = process.env.OPENKBS_KB_ID;
216
+ const apiKey = process.env.OPENKBS_API_KEY;
217
+
218
+ switch (action) {
219
+ case 'getPulseToken': {
220
+ const tokenData = await pulse.getToken(kbId, apiKey, data.userId);
221
+ return {
222
+ statusCode: 200,
223
+ headers,
224
+ body: JSON.stringify({ ...tokenData, kbId })
225
+ };
226
+ }
227
+
228
+ case 'list': {
229
+ const { rows } = await db.query(
230
+ 'SELECT * FROM posts ORDER BY created_at DESC LIMIT 50'
231
+ );
232
+ return { statusCode: 200, headers, body: JSON.stringify({ posts: rows }) };
233
+ }
234
+
235
+ case 'create': {
236
+ const result = await db.query(
237
+ 'INSERT INTO posts (user_name, content) VALUES ($1, $2) RETURNING *',
238
+ [data.userName, data.content]
239
+ );
240
+ const post = result.rows[0];
241
+
242
+ // Broadcast to all subscribers
243
+ await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
244
+
245
+ return { statusCode: 200, headers, body: JSON.stringify({ post }) };
246
+ }
247
+
248
+ case 'presence': {
249
+ const result = await pulse.presence('posts', { kbId, apiKey });
250
+ return { statusCode: 200, headers, body: JSON.stringify(result) };
251
+ }
252
+
253
+ default:
254
+ return { statusCode: 400, headers, body: JSON.stringify({ error: 'Unknown action' }) };
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### Frontend
260
+
261
+ ```html
262
+ <!DOCTYPE html>
263
+ <html>
264
+ <head>
265
+ <title>Live Posts</title>
266
+ <script src="https://unpkg.com/openkbs-pulse@2.0.1/pulse.js"></script>
267
+ </head>
268
+ <body>
269
+ <div id="online">Online: 0</div>
270
+ <div id="posts"></div>
271
+
272
+ <form id="form">
273
+ <input type="text" id="content" placeholder="What's on your mind?">
274
+ <button type="submit">Post</button>
275
+ </form>
276
+
277
+ <script>
278
+ const userId = 'user_' + Math.random().toString(36).substr(2, 9);
279
+ let realtime;
280
+
281
+ async function init() {
282
+ // Load existing posts
283
+ const { posts } = await fetch('/posts', {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ action: 'list' })
287
+ }).then(r => r.json());
288
+
289
+ posts.forEach(addPost);
290
+
291
+ // Get Pulse token
292
+ const { token, endpoint, kbId } = await fetch('/posts', {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({ action: 'getPulseToken', userId })
296
+ }).then(r => r.json());
297
+
298
+ // Connect
299
+ realtime = new Pulse.Realtime({ kbId, token, endpoint, clientId: userId });
300
+
301
+ const channel = realtime.channels.get('posts');
302
+
303
+ // Subscribe to new posts
304
+ channel.subscribe('new_post', (msg) => {
305
+ addPost(msg.data.post);
306
+ });
307
+
308
+ // Presence
309
+ channel.presence.enter({ name: userId });
310
+ channel.presence.subscribe((members) => {
311
+ document.getElementById('online').textContent = 'Online: ' + members.length;
312
+ });
313
+ }
314
+
315
+ function addPost(post) {
316
+ const div = document.createElement('div');
317
+ div.innerHTML = `<b>${post.user_name}</b>: ${post.content}`;
318
+ document.getElementById('posts').prepend(div);
319
+ }
320
+
321
+ document.getElementById('form').addEventListener('submit', async (e) => {
322
+ e.preventDefault();
323
+ const content = document.getElementById('content').value;
324
+ if (!content) return;
325
+
326
+ await fetch('/posts', {
327
+ method: 'POST',
328
+ headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ action: 'create', content, userName: userId })
330
+ });
331
+
332
+ document.getElementById('content').value = '';
333
+ });
334
+
335
+ init();
336
+ </script>
337
+ </body>
338
+ </html>
339
+ ```
340
+
341
+ ## Private Channels
342
+
343
+ For private messaging, use secret channel IDs:
344
+
345
+ ```javascript
346
+ // Generate unique channel ID per user (store in database)
347
+ const privateChannel = crypto.randomBytes(32).toString('hex');
348
+
349
+ // User subscribes to their own private channel
350
+ const myChannel = realtime.channels.get(user.privateChannel);
351
+ myChannel.subscribe('message', (msg) => {
352
+ console.log('Private message:', msg.data);
353
+ });
354
+
355
+ // Server publishes to recipient's channel
356
+ await pulse.publish(recipient.privateChannel, 'message', {
357
+ from: sender.name,
358
+ text: 'Hello!'
359
+ }, { kbId, apiKey });
360
+ ```
361
+
362
+ ## CLI Reference
363
+
364
+ ```bash
365
+ openkbs pulse enable # Enable Pulse
366
+ openkbs pulse status # Check status
367
+ openkbs pulse channels # List active channels
368
+ openkbs pulse presence <channel> # View presence
369
+ openkbs pulse publish <ch> "msg" # Publish message
370
+ openkbs pulse disable # Disable
371
+ ```
372
+
373
+ ## Tips
374
+
375
+ 1. **Generate Tokens Server-Side** - Never expose your API key in frontend code.
376
+
377
+ 2. **Use Presence for Online Status** - Built-in, no database needed.
378
+
379
+ 3. **Private Channels** - Use random IDs for 1:1 messaging.
380
+
381
+ 4. **Reconnection** - SDK auto-reconnects with exponential backoff.
382
+
383
+ ## Next Steps
384
+
385
+ - [Tutorial 15: Node.js Full Example](./15-nodejs-example.md)
386
+ - [Tutorial 16: Java Example](./16-java-example.md)
@@ -0,0 +1,291 @@
1
+ # Tutorial 12: S3 Storage with CloudFront CDN
2
+
3
+ Upload files, serve images, and host media with S3 storage and CloudFront CDN. Get presigned URLs for secure browser uploads.
4
+
5
+ ## Enable Storage
6
+
7
+ ```bash
8
+ openkbs storage enable
9
+ ```
10
+
11
+ Your S3 bucket is ready.
12
+
13
+ ## Check Status
14
+
15
+ ```bash
16
+ openkbs storage status
17
+ ```
18
+
19
+ Output:
20
+ ```
21
+ Storage Status:
22
+ Enabled: true
23
+ Bucket: openkbs-elastic-abc123
24
+ Region: us-east-1
25
+ Public: false
26
+ ```
27
+
28
+ ## Add CloudFront CDN
29
+
30
+ Serve files from your domain with edge caching:
31
+
32
+ ```bash
33
+ openkbs storage cloudfront media
34
+ ```
35
+
36
+ This maps:
37
+ - S3 path `media/*` to URL `yourdomain.com/media/*`
38
+
39
+ | S3 Key | Public URL |
40
+ |--------|------------|
41
+ | `media/photo.jpg` | `yourdomain.com/media/photo.jpg` |
42
+ | `media/uploads/image.png` | `yourdomain.com/media/uploads/image.png` |
43
+
44
+ ## Upload Files
45
+
46
+ ### From CLI
47
+
48
+ ```bash
49
+ openkbs storage put ./photo.jpg media/photo.jpg
50
+ openkbs storage put ./document.pdf docs/document.pdf
51
+ ```
52
+
53
+ ### From Lambda (Presigned URL)
54
+
55
+ Generate a presigned URL for browser uploads:
56
+
57
+ ```javascript
58
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
59
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
60
+
61
+ const s3 = new S3Client({ region: process.env.STORAGE_REGION });
62
+
63
+ export async function handler(event) {
64
+ const { action, fileName, contentType } = JSON.parse(event.body || '{}');
65
+
66
+ if (action === 'getUploadUrl') {
67
+ const bucket = process.env.STORAGE_BUCKET;
68
+
69
+ // Key must match CloudFront path prefix
70
+ const key = `media/uploads/${Date.now()}-${fileName}`;
71
+
72
+ const command = new PutObjectCommand({
73
+ Bucket: bucket,
74
+ Key: key,
75
+ ContentType: contentType || 'application/octet-stream'
76
+ });
77
+
78
+ const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
79
+
80
+ // Return relative URL for CloudFront
81
+ const publicUrl = `/${key}`;
82
+
83
+ return {
84
+ statusCode: 200,
85
+ body: JSON.stringify({ uploadUrl, publicUrl, key })
86
+ };
87
+ }
88
+ }
89
+ ```
90
+
91
+ `package.json`:
92
+ ```json
93
+ {
94
+ "type": "module",
95
+ "dependencies": {
96
+ "@aws-sdk/client-s3": "^3.400.0",
97
+ "@aws-sdk/s3-request-presigner": "^3.400.0"
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Browser Upload
103
+
104
+ ```javascript
105
+ async function uploadFile(file) {
106
+ // 1. Get presigned URL from your API
107
+ const response = await fetch('/api', {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({
111
+ action: 'getUploadUrl',
112
+ fileName: file.name,
113
+ contentType: file.type
114
+ })
115
+ });
116
+
117
+ const { uploadUrl, publicUrl } = await response.json();
118
+
119
+ // 2. Upload directly to S3
120
+ await fetch(uploadUrl, {
121
+ method: 'PUT',
122
+ body: file,
123
+ headers: { 'Content-Type': file.type }
124
+ });
125
+
126
+ // 3. Return the public URL
127
+ return publicUrl; // e.g., /media/uploads/1234567890-photo.jpg
128
+ }
129
+
130
+ // Usage
131
+ const fileInput = document.querySelector('input[type="file"]');
132
+ fileInput.addEventListener('change', async (e) => {
133
+ const file = e.target.files[0];
134
+ const url = await uploadFile(file);
135
+ console.log('Uploaded to:', url);
136
+ });
137
+ ```
138
+
139
+ ### Upload with Progress
140
+
141
+ ```javascript
142
+ function uploadWithProgress(file, onProgress) {
143
+ return new Promise(async (resolve, reject) => {
144
+ const { uploadUrl, publicUrl } = await fetch('/api', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({
148
+ action: 'getUploadUrl',
149
+ fileName: file.name,
150
+ contentType: file.type
151
+ })
152
+ }).then(r => r.json());
153
+
154
+ const xhr = new XMLHttpRequest();
155
+
156
+ xhr.upload.addEventListener('progress', (e) => {
157
+ if (e.lengthComputable) {
158
+ onProgress(Math.round((e.loaded / e.total) * 100));
159
+ }
160
+ });
161
+
162
+ xhr.addEventListener('load', () => {
163
+ if (xhr.status === 200) resolve(publicUrl);
164
+ else reject(new Error('Upload failed'));
165
+ });
166
+
167
+ xhr.addEventListener('error', reject);
168
+ xhr.open('PUT', uploadUrl);
169
+ xhr.setRequestHeader('Content-Type', file.type);
170
+ xhr.send(file);
171
+ });
172
+ }
173
+
174
+ // Usage
175
+ const url = await uploadWithProgress(file, (percent) => {
176
+ console.log(`Upload: ${percent}%`);
177
+ });
178
+ ```
179
+
180
+ ## List Files
181
+
182
+ ```bash
183
+ openkbs storage ls
184
+ openkbs storage ls media/
185
+ openkbs storage ls media/uploads/ --limit 100
186
+ ```
187
+
188
+ ## Download Files
189
+
190
+ ```bash
191
+ openkbs storage get media/photo.jpg
192
+ openkbs storage get media/photo.jpg ./local-photo.jpg
193
+ ```
194
+
195
+ ## Delete Files
196
+
197
+ ```bash
198
+ openkbs storage rm media/photo.jpg
199
+ ```
200
+
201
+ ## Make Bucket Public
202
+
203
+ By default, buckets are private. To make files publicly accessible without presigned URLs:
204
+
205
+ ```bash
206
+ openkbs storage public true
207
+ ```
208
+
209
+ To make private again:
210
+ ```bash
211
+ openkbs storage public false
212
+ ```
213
+
214
+ ## File Organization
215
+
216
+ Recommended structure:
217
+ ```
218
+ media/
219
+ images/
220
+ avatars/
221
+ posts/
222
+ videos/
223
+ documents/
224
+ uploads/
225
+ temp/
226
+ ```
227
+
228
+ ## Python Example
229
+
230
+ ```python
231
+ import json
232
+ import os
233
+ import boto3
234
+ from botocore.config import Config
235
+
236
+ s3 = boto3.client('s3',
237
+ region_name=os.environ.get('STORAGE_REGION', 'us-east-1'),
238
+ config=Config(signature_version='s3v4')
239
+ )
240
+
241
+ def handler(event, context):
242
+ body = json.loads(event.get('body', '{}'))
243
+ action = body.get('action')
244
+ bucket = os.environ['STORAGE_BUCKET']
245
+
246
+ if action == 'getUploadUrl':
247
+ import time
248
+ key = f"media/uploads/{int(time.time())}-{body['fileName']}"
249
+
250
+ url = s3.generate_presigned_url('put_object',
251
+ Params={'Bucket': bucket, 'Key': key, 'ContentType': body.get('contentType', 'application/octet-stream')},
252
+ ExpiresIn=3600
253
+ )
254
+
255
+ return {
256
+ 'statusCode': 200,
257
+ 'body': json.dumps({'uploadUrl': url, 'publicUrl': f'/{key}', 'key': key})
258
+ }
259
+
260
+ return {'statusCode': 400, 'body': json.dumps({'error': 'Unknown action'})}
261
+ ```
262
+
263
+ ## CLI Reference
264
+
265
+ ```bash
266
+ openkbs storage enable # Enable storage
267
+ openkbs storage status # Check status
268
+ openkbs storage public true|false # Set public access
269
+ openkbs storage cloudfront <path> # Add CloudFront path
270
+ openkbs storage cloudfront remove <path> # Remove CloudFront path
271
+ openkbs storage ls [prefix] # List files
272
+ openkbs storage put <file> [key] # Upload file
273
+ openkbs storage get <key> [file] # Download file
274
+ openkbs storage rm <key> # Delete file
275
+ openkbs storage disable --force # Disable (DANGEROUS)
276
+ ```
277
+
278
+ ## Tips
279
+
280
+ 1. **Always Use Presigned URLs** - Don't send file data through your Lambda. Upload directly to S3.
281
+
282
+ 2. **Match CloudFront Path** - If CloudFront path is `media`, upload to `media/...` keys.
283
+
284
+ 3. **Use Content-Type** - Set correct MIME type for proper browser handling.
285
+
286
+ 4. **Versioned Filenames** - Use timestamps or UUIDs to avoid cache issues: `1704067200-photo.jpg`
287
+
288
+ ## Next Steps
289
+
290
+ - [Tutorial 13: Serverless Functions](./13-functions.md)
291
+ - [Tutorial 14: Real-time Pulse](./14-pulse.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openkbs",
3
- "version": "0.0.64",
3
+ "version": "0.0.66",
4
4
  "description": "OpenKBS - Command Line Interface",
5
5
  "main": "src/index.js",
6
6
  "scripts": {