@scalemule/sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +424 -0
- package/dist/chunk-3FTGBRLU.mjs +158 -0
- package/dist/index.d.mts +4014 -0
- package/dist/index.d.ts +4014 -0
- package/dist/index.js +4666 -0
- package/dist/index.mjs +4309 -0
- package/dist/upload-compression-CWKEDQYS.mjs +108 -0
- package/dist/upload-resume-RXLHBH5E.mjs +6 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScaleMule Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @scalemule/sdk
|
|
2
|
+
|
|
3
|
+
Official TypeScript/JavaScript SDK for ScaleMule Backend-as-a-Service.
|
|
4
|
+
|
|
5
|
+
Zero dependencies. Works in browsers, Node.js 18+, and edge runtimes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @scalemule/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ScaleMule } from '@scalemule/sdk'
|
|
17
|
+
|
|
18
|
+
const sm = new ScaleMule({ apiKey: 'sm_pb_xxx' })
|
|
19
|
+
|
|
20
|
+
// Register
|
|
21
|
+
const { data, error } = await sm.auth.register({
|
|
22
|
+
email: 'user@example.com',
|
|
23
|
+
password: 'SecurePassword123!',
|
|
24
|
+
name: 'Jane Doe',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (error) {
|
|
28
|
+
console.error(error.code, error.message)
|
|
29
|
+
} else {
|
|
30
|
+
console.log('User created:', data.id)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every method returns `{ data, error }` — never throws on API errors.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const sm = new ScaleMule({
|
|
40
|
+
apiKey: 'sm_pb_xxx', // required
|
|
41
|
+
environment: 'prod', // 'dev' | 'prod' (default: 'prod')
|
|
42
|
+
baseUrl: 'https://api.example.com', // overrides environment preset
|
|
43
|
+
timeout: 30000, // request timeout in ms
|
|
44
|
+
retry: {
|
|
45
|
+
maxRetries: 2, // retry on 429/5xx (default: 2)
|
|
46
|
+
backoffMs: 300, // base delay with jitter (default: 300)
|
|
47
|
+
},
|
|
48
|
+
enableRateLimitQueue: true, // auto-queue on 429
|
|
49
|
+
enableOfflineQueue: true, // queue when offline, sync on reconnect
|
|
50
|
+
debug: false, // log requests to console
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Environment | Gateway URL |
|
|
55
|
+
|-------------|-------------|
|
|
56
|
+
| `prod` | `https://api.scalemule.com` |
|
|
57
|
+
| `dev` | `https://api-dev.scalemule.com` |
|
|
58
|
+
|
|
59
|
+
## Response Contract
|
|
60
|
+
|
|
61
|
+
All methods return `ApiResponse<T>`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
type ApiResponse<T> = {
|
|
65
|
+
data: T | null // null when error is present
|
|
66
|
+
error: ApiError | null // null on success
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ApiError = {
|
|
70
|
+
code: string // machine-readable (e.g., 'unauthorized')
|
|
71
|
+
message: string // human-readable
|
|
72
|
+
status: number // HTTP status code
|
|
73
|
+
details?: Record<string, unknown>
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Paginated methods return `PaginatedResponse<T>`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
type PaginatedResponse<T> = {
|
|
81
|
+
data: T[]
|
|
82
|
+
metadata: { total: number; totalPages: number; page: number; perPage: number }
|
|
83
|
+
error: ApiError | null
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Auth
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Register & Login
|
|
91
|
+
const { data } = await sm.auth.register({ email, password, name })
|
|
92
|
+
const { data } = await sm.auth.login({ email, password })
|
|
93
|
+
await sm.auth.logout()
|
|
94
|
+
|
|
95
|
+
// Current user
|
|
96
|
+
const { data: user } = await sm.auth.me()
|
|
97
|
+
|
|
98
|
+
// Session management
|
|
99
|
+
await sm.auth.refreshSession()
|
|
100
|
+
sm.setAccessToken(token)
|
|
101
|
+
sm.clearAccessToken()
|
|
102
|
+
|
|
103
|
+
// Password
|
|
104
|
+
await sm.auth.forgotPassword({ email })
|
|
105
|
+
await sm.auth.resetPassword({ token, newPassword })
|
|
106
|
+
await sm.auth.changePassword({ currentPassword, newPassword })
|
|
107
|
+
|
|
108
|
+
// Email verification
|
|
109
|
+
await sm.auth.verifyEmail({ token })
|
|
110
|
+
await sm.auth.resendVerification()
|
|
111
|
+
|
|
112
|
+
// Phone OTP
|
|
113
|
+
await sm.auth.sendPhoneOtp({ phone, purpose: 'verify_phone' })
|
|
114
|
+
await sm.auth.verifyPhoneOtp({ phone, code })
|
|
115
|
+
await sm.auth.loginWithPhone({ phone, code })
|
|
116
|
+
// SDK auto-sanitizes formatting before send:
|
|
117
|
+
// "(415) 555-1234" -> "+4155551234"
|
|
118
|
+
|
|
119
|
+
// OAuth
|
|
120
|
+
const { data } = await sm.auth.getOAuthUrl('google', { redirectUri })
|
|
121
|
+
const { data } = await sm.auth.handleOAuthCallback({ provider, code, state })
|
|
122
|
+
|
|
123
|
+
// Sessions, Devices, Login History
|
|
124
|
+
const { data } = await sm.auth.sessions.list()
|
|
125
|
+
await sm.auth.sessions.revoke(sessionId)
|
|
126
|
+
const { data } = await sm.auth.devices.list()
|
|
127
|
+
const { data } = await sm.auth.loginHistory.list()
|
|
128
|
+
|
|
129
|
+
// MFA
|
|
130
|
+
const { data } = await sm.auth.mfa.getStatus()
|
|
131
|
+
const { data } = await sm.auth.mfa.setupTotp()
|
|
132
|
+
await sm.auth.mfa.verifySetup({ code })
|
|
133
|
+
await sm.auth.mfa.disable({ password })
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Phone Country Picker Helpers
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { PHONE_COUNTRIES, composePhoneNumber, normalizeAndValidatePhone } from '@scalemule/sdk'
|
|
140
|
+
|
|
141
|
+
const us = PHONE_COUNTRIES.find((country) => country.code === 'US')
|
|
142
|
+
const phone = composePhoneNumber(us?.dialCode ?? '+1', '(415) 555-1234')
|
|
143
|
+
const normalized = normalizeAndValidatePhone(phone)
|
|
144
|
+
|
|
145
|
+
if (!normalized.valid) {
|
|
146
|
+
console.error(normalized.error)
|
|
147
|
+
} else {
|
|
148
|
+
await sm.auth.register({ email, password, phone: normalized.normalized! })
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Storage
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// Upload (3-step presigned URL flow, hidden from you)
|
|
156
|
+
const { data, error } = await sm.storage.upload(file, {
|
|
157
|
+
filename: 'photo.jpg',
|
|
158
|
+
isPublic: false,
|
|
159
|
+
onProgress: (pct) => console.log(`${pct}%`),
|
|
160
|
+
signal: abortController.signal,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Signed URLs for viewing/downloading
|
|
164
|
+
const { data } = await sm.storage.getViewUrl(fileId)
|
|
165
|
+
const { data: urls } = await sm.storage.getViewUrls(fileIds) // batch, up to 100
|
|
166
|
+
const { data } = await sm.storage.getDownloadUrl(fileId)
|
|
167
|
+
|
|
168
|
+
// File operations
|
|
169
|
+
const { data: info } = await sm.storage.getInfo(fileId)
|
|
170
|
+
const { data: files, metadata } = await sm.storage.list({ page: 1, perPage: 20 })
|
|
171
|
+
await sm.storage.delete(fileId)
|
|
172
|
+
|
|
173
|
+
// Split upload (server-side: get URL → client uploads to S3 → complete)
|
|
174
|
+
const { data: upload } = await sm.storage.getUploadUrl('photo.jpg', 'image/jpeg')
|
|
175
|
+
// ... client uploads to upload.upload_url ...
|
|
176
|
+
const { data: result } = await sm.storage.completeUpload(upload.file_id, upload.completion_token)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Data
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// CRUD
|
|
183
|
+
const { data: doc } = await sm.data.create('todos', { title: 'Ship SDK', done: false })
|
|
184
|
+
const { data: doc } = await sm.data.get('todos', docId)
|
|
185
|
+
await sm.data.update('todos', docId, { done: true })
|
|
186
|
+
await sm.data.delete('todos', docId)
|
|
187
|
+
|
|
188
|
+
// Query with filters and sorting
|
|
189
|
+
const { data: docs, metadata } = await sm.data.query('todos', {
|
|
190
|
+
filters: [{ operator: 'eq', field: 'done', value: false }],
|
|
191
|
+
sort: [{ field: 'created_at', direction: 'desc' }],
|
|
192
|
+
page: 1,
|
|
193
|
+
perPage: 20,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Collections
|
|
197
|
+
await sm.data.createCollection('todos')
|
|
198
|
+
const { data } = await sm.data.listCollections()
|
|
199
|
+
|
|
200
|
+
// My documents (filtered by current user)
|
|
201
|
+
const { data } = await sm.data.myDocuments('todos')
|
|
202
|
+
|
|
203
|
+
// Aggregations
|
|
204
|
+
const { data } = await sm.data.aggregate('orders', {
|
|
205
|
+
pipeline: [{ $group: { field: 'status', fn: 'count' } }],
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Filter operators**: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`
|
|
210
|
+
|
|
211
|
+
## Video
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const { data } = await sm.video.upload(videoFile, {
|
|
215
|
+
onProgress: (pct) => console.log(`${pct}%`),
|
|
216
|
+
})
|
|
217
|
+
const { data } = await sm.video.get(videoId)
|
|
218
|
+
const { data } = await sm.video.getStreamUrl(videoId)
|
|
219
|
+
await sm.video.trackPlayback(videoId, { event: 'play', position: 0 })
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Realtime
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Subscribe (auto-connects WebSocket on first call)
|
|
226
|
+
const unsub = sm.realtime.subscribe('chat:room-1', (msg) => {
|
|
227
|
+
console.log('Message:', msg)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// Publish
|
|
231
|
+
await sm.realtime.publish('chat:room-1', { text: 'hello' })
|
|
232
|
+
|
|
233
|
+
// Connection status
|
|
234
|
+
console.log(sm.realtime.status) // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
235
|
+
|
|
236
|
+
// Cleanup
|
|
237
|
+
unsub()
|
|
238
|
+
sm.realtime.disconnect()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Features: auto-reconnect with backoff, re-subscribe on reconnect, heartbeat/ping-pong.
|
|
242
|
+
|
|
243
|
+
## Chat
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const { data: convo } = await sm.chat.createConversation({ participantIds: [a, b] })
|
|
247
|
+
const { data: msg } = await sm.chat.sendMessage(convo.id, { content: 'Hello!' })
|
|
248
|
+
const { data: msgs } = await sm.chat.getMessages(convo.id, { limit: 50 })
|
|
249
|
+
await sm.chat.sendTyping(convo.id)
|
|
250
|
+
await sm.chat.markRead(convo.id)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Social
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
await sm.social.follow(userId)
|
|
257
|
+
await sm.social.unfollow(userId)
|
|
258
|
+
const { data: post } = await sm.social.createPost({ content: 'Hello!', visibility: 'public' })
|
|
259
|
+
const { data: feed } = await sm.social.getFeed({ limit: 20 })
|
|
260
|
+
await sm.social.like('post', postId)
|
|
261
|
+
const { data } = await sm.social.getComments(postId)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Teams
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const { data: team } = await sm.teams.create({ name: 'Engineering' })
|
|
268
|
+
await sm.teams.invite(team.id, { email: 'dev@company.com', role: 'member' })
|
|
269
|
+
await sm.teams.acceptInvitation(token)
|
|
270
|
+
const { data: members } = await sm.teams.listMembers(team.id)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Billing
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const { data: customer } = await sm.billing.createCustomer({ email, name })
|
|
277
|
+
const { data: sub } = await sm.billing.subscribe({ customerId, planId })
|
|
278
|
+
await sm.billing.reportUsage({ metric: 'api_calls', quantity: 1000 })
|
|
279
|
+
const { data: invoices } = await sm.billing.listInvoices()
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Analytics
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
await sm.analytics.track('button_clicked', { buttonId: 'signup' })
|
|
286
|
+
await sm.analytics.trackPageView({ path: '/pricing' })
|
|
287
|
+
await sm.analytics.trackBatch([
|
|
288
|
+
{ event: 'page_view', properties: { path: '/home' } },
|
|
289
|
+
{ event: 'page_view', properties: { path: '/pricing' } },
|
|
290
|
+
])
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## More Services
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
// Queue
|
|
297
|
+
await sm.queue.enqueue({ job_type: 'email.welcome', payload: { userId }, priority: 'high' })
|
|
298
|
+
|
|
299
|
+
// Scheduler
|
|
300
|
+
const { data } = await sm.scheduler.createJob({
|
|
301
|
+
name: 'daily-report', cron: '0 9 * * *',
|
|
302
|
+
type: 'webhook', config: { url: 'https://myapp.com/api/report' },
|
|
303
|
+
})
|
|
304
|
+
await sm.scheduler.pauseJob(jobId)
|
|
305
|
+
await sm.scheduler.runNow(jobId)
|
|
306
|
+
|
|
307
|
+
// Permissions
|
|
308
|
+
await sm.permissions.createRole({ name: 'editor' })
|
|
309
|
+
await sm.permissions.assignPermissions(roleId, ['posts.create', 'posts.edit'])
|
|
310
|
+
const { data: { allowed } } = await sm.permissions.check(userId, 'posts.create')
|
|
311
|
+
|
|
312
|
+
// Communication
|
|
313
|
+
await sm.communication.sendEmail({ to: 'user@example.com', subject: 'Welcome!', body: '<h1>Hi</h1>' })
|
|
314
|
+
await sm.communication.sendEmailTemplate('welcome', { to: email, variables: { name: 'Jane' } })
|
|
315
|
+
|
|
316
|
+
// Search
|
|
317
|
+
await sm.search.index('products', { id: '1', name: 'Widget', price: 9.99 })
|
|
318
|
+
const { data } = await sm.search.query('products', { query: 'widget', limit: 10 })
|
|
319
|
+
|
|
320
|
+
// Cache
|
|
321
|
+
await sm.cache.set('key', { value: 'data' }, { ttl: 3600 })
|
|
322
|
+
const { data } = await sm.cache.get('key')
|
|
323
|
+
|
|
324
|
+
// Graph
|
|
325
|
+
const { data: node } = await sm.graph.createNode({ label: 'person', properties: { name: 'Alice' } })
|
|
326
|
+
const { data: edge } = await sm.graph.createEdge({ fromId: a, toId: b, type: 'knows' })
|
|
327
|
+
|
|
328
|
+
// Content Moderation
|
|
329
|
+
await sm.flagContent.createFlag({ content_type: 'post', content_id: postId, category: 'spam' })
|
|
330
|
+
|
|
331
|
+
// Webhooks
|
|
332
|
+
await sm.webhooks.create({ url: 'https://myapp.com/hooks', events: ['auth.user.created'] })
|
|
333
|
+
|
|
334
|
+
// Leaderboard
|
|
335
|
+
await sm.leaderboard.submitScore(boardId, { userId, score: 1500 })
|
|
336
|
+
|
|
337
|
+
// Listings
|
|
338
|
+
const { data } = await sm.listings.nearby({ lat: 40.7, lng: -74.0, radius: 10 })
|
|
339
|
+
|
|
340
|
+
// Events
|
|
341
|
+
const { data: event } = await sm.events.create({ title: 'Launch Party', startDate: '2026-03-01' })
|
|
342
|
+
|
|
343
|
+
// Functions
|
|
344
|
+
const { data } = await sm.functions.invoke('resize', { imageUrl: '...', width: 200 })
|
|
345
|
+
|
|
346
|
+
// Photo
|
|
347
|
+
const { data } = await sm.photo.upload(imageFile)
|
|
348
|
+
|
|
349
|
+
// Compliance
|
|
350
|
+
await sm.compliance.log({ action: 'user.deleted', resourceType: 'user', resourceId: id })
|
|
351
|
+
|
|
352
|
+
// Orchestrator
|
|
353
|
+
const { data } = await sm.orchestrator.execute(workflowId, { userId })
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## All 30 Services
|
|
357
|
+
|
|
358
|
+
| Service | Property | Description |
|
|
359
|
+
|---------|----------|-------------|
|
|
360
|
+
| Auth | `sm.auth` | Authentication, sessions, MFA, OAuth, phone |
|
|
361
|
+
| Storage | `sm.storage` | File upload (presigned S3), signed URLs |
|
|
362
|
+
| Realtime | `sm.realtime` | WebSocket pub/sub with auto-reconnect |
|
|
363
|
+
| Video | `sm.video` | Video upload, streaming, analytics |
|
|
364
|
+
| Data | `sm.data` | Document CRUD, queries, aggregations |
|
|
365
|
+
| Chat | `sm.chat` | Conversations, messages, typing, read receipts |
|
|
366
|
+
| Social | `sm.social` | Follow, posts, feed, likes, comments |
|
|
367
|
+
| Billing | `sm.billing` | Customers, subscriptions, invoices, usage |
|
|
368
|
+
| Analytics | `sm.analytics` | Event tracking, page views, funnels |
|
|
369
|
+
| Communication | `sm.communication` | Email, SMS, push notifications |
|
|
370
|
+
| Scheduler | `sm.scheduler` | Cron jobs, one-time jobs |
|
|
371
|
+
| Permissions | `sm.permissions` | Roles, permissions, RBAC |
|
|
372
|
+
| Teams | `sm.teams` | Team management, invitations, SSO |
|
|
373
|
+
| Accounts | `sm.accounts` | Client/application management |
|
|
374
|
+
| Identity | `sm.identity` | API key management |
|
|
375
|
+
| Cache | `sm.cache` | Redis key-value cache |
|
|
376
|
+
| Queue | `sm.queue` | Async job processing |
|
|
377
|
+
| Search | `sm.search` | Full-text search |
|
|
378
|
+
| Webhooks | `sm.webhooks` | Webhook management |
|
|
379
|
+
| Graph | `sm.graph` | Graph database (nodes, edges, traversal) |
|
|
380
|
+
| Functions | `sm.functions` | Serverless function execution |
|
|
381
|
+
| Listings | `sm.listings` | Marketplace listings |
|
|
382
|
+
| Events | `sm.events` | Event management, registration |
|
|
383
|
+
| Leaderboard | `sm.leaderboard` | Gamification leaderboards |
|
|
384
|
+
| Photo | `sm.photo` | Image processing |
|
|
385
|
+
| Flag Content | `sm.flagContent` | Content moderation |
|
|
386
|
+
| Compliance | `sm.compliance` | GDPR, audit logs |
|
|
387
|
+
| Orchestrator | `sm.orchestrator` | Workflow automation |
|
|
388
|
+
| Logger | `sm.logger` | Centralized logging |
|
|
389
|
+
| Catalog | `sm.catalog` | Service registry |
|
|
390
|
+
|
|
391
|
+
## Error Codes
|
|
392
|
+
|
|
393
|
+
| Code | HTTP | When |
|
|
394
|
+
|------|------|------|
|
|
395
|
+
| `unauthorized` | 401 | Missing or invalid auth |
|
|
396
|
+
| `forbidden` | 403 | Valid auth, insufficient permissions |
|
|
397
|
+
| `not_found` | 404 | Resource doesn't exist |
|
|
398
|
+
| `validation_error` | 422 | Bad input (`details` has per-field errors) |
|
|
399
|
+
| `rate_limited` | 429 | Too many requests (`details.retryAfter`) |
|
|
400
|
+
| `conflict` | 409 | Duplicate resource |
|
|
401
|
+
| `file_scanning` | 202 | File uploaded, scan not complete |
|
|
402
|
+
| `file_threat` | 403 | Malware detected |
|
|
403
|
+
| `internal_error` | 500 | Server error |
|
|
404
|
+
|
|
405
|
+
## TypeScript
|
|
406
|
+
|
|
407
|
+
Full type definitions included. All service methods, request/response types, and error codes are typed.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import type {
|
|
411
|
+
ScaleMuleConfig,
|
|
412
|
+
ApiResponse,
|
|
413
|
+
ApiError,
|
|
414
|
+
PaginatedResponse,
|
|
415
|
+
QueryFilter,
|
|
416
|
+
QuerySort,
|
|
417
|
+
PresignedUploadResponse,
|
|
418
|
+
UploadCompleteResponse,
|
|
419
|
+
} from '@scalemule/sdk'
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
Proprietary - ScaleMule Inc.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/services/upload-resume.ts
|
|
2
|
+
var DB_NAME = "sm_upload_sessions_v1";
|
|
3
|
+
var STORE_NAME = "sessions";
|
|
4
|
+
var DB_VERSION = 1;
|
|
5
|
+
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
6
|
+
var UploadResumeStore = class {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.db = null;
|
|
9
|
+
}
|
|
10
|
+
/** Generate a deterministic resume key from upload identity */
|
|
11
|
+
static async generateResumeKey(appId, userId, filename, size, lastModified) {
|
|
12
|
+
const raw = `${appId}:${userId}:${filename}:${size}:${lastModified ?? 0}`;
|
|
13
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
14
|
+
const buffer = new TextEncoder().encode(raw);
|
|
15
|
+
const hash2 = await crypto.subtle.digest("SHA-256", buffer);
|
|
16
|
+
return Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
}
|
|
18
|
+
let hash = 0;
|
|
19
|
+
for (let i = 0; i < raw.length; i++) {
|
|
20
|
+
const chr = raw.charCodeAt(i);
|
|
21
|
+
hash = (hash << 5) - hash + chr;
|
|
22
|
+
hash |= 0;
|
|
23
|
+
}
|
|
24
|
+
return `fallback_${Math.abs(hash).toString(36)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Open the IndexedDB store. No-ops if IndexedDB is unavailable. */
|
|
27
|
+
async open() {
|
|
28
|
+
if (typeof indexedDB === "undefined") return;
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
31
|
+
request.onupgradeneeded = () => {
|
|
32
|
+
const db = request.result;
|
|
33
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
34
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
35
|
+
store.createIndex("updated_at", "updated_at");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
request.onsuccess = () => {
|
|
39
|
+
this.db = request.result;
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
request.onerror = () => {
|
|
43
|
+
reject(request.error);
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Get a resume session by key. Returns null if not found or stale. */
|
|
48
|
+
async get(key) {
|
|
49
|
+
if (!this.db) return null;
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const tx = this.db.transaction(STORE_NAME, "readonly");
|
|
52
|
+
const store = tx.objectStore(STORE_NAME);
|
|
53
|
+
const request = store.get(key);
|
|
54
|
+
request.onsuccess = () => {
|
|
55
|
+
const entry = request.result;
|
|
56
|
+
if (!entry) {
|
|
57
|
+
resolve(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Date.now() - entry.updated_at > MAX_AGE_MS) {
|
|
61
|
+
this.remove(key).catch(() => {
|
|
62
|
+
});
|
|
63
|
+
resolve(null);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
resolve(entry.session);
|
|
67
|
+
};
|
|
68
|
+
request.onerror = () => resolve(null);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Save a new resume session. */
|
|
72
|
+
async save(key, session) {
|
|
73
|
+
if (!this.db) return;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
76
|
+
const store = tx.objectStore(STORE_NAME);
|
|
77
|
+
const entry = {
|
|
78
|
+
key,
|
|
79
|
+
session: { ...session, created_at: Date.now() },
|
|
80
|
+
updated_at: Date.now()
|
|
81
|
+
};
|
|
82
|
+
const request = store.put(entry);
|
|
83
|
+
request.onsuccess = () => resolve();
|
|
84
|
+
request.onerror = () => reject(request.error);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Update a single completed part in an existing session. */
|
|
88
|
+
async updatePart(key, partNumber, etag) {
|
|
89
|
+
if (!this.db) return;
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
92
|
+
const store = tx.objectStore(STORE_NAME);
|
|
93
|
+
const getRequest = store.get(key);
|
|
94
|
+
getRequest.onsuccess = () => {
|
|
95
|
+
const entry = getRequest.result;
|
|
96
|
+
if (!entry) {
|
|
97
|
+
resolve();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const existing = entry.session.completed_parts.find((p) => p.part_number === partNumber);
|
|
101
|
+
if (!existing) {
|
|
102
|
+
entry.session.completed_parts.push({ part_number: partNumber, etag });
|
|
103
|
+
}
|
|
104
|
+
entry.updated_at = Date.now();
|
|
105
|
+
const putRequest = store.put(entry);
|
|
106
|
+
putRequest.onsuccess = () => resolve();
|
|
107
|
+
putRequest.onerror = () => resolve();
|
|
108
|
+
};
|
|
109
|
+
getRequest.onerror = () => resolve();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Remove a resume session (e.g., after successful completion). */
|
|
113
|
+
async remove(key) {
|
|
114
|
+
if (!this.db) return;
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
117
|
+
const store = tx.objectStore(STORE_NAME);
|
|
118
|
+
const request = store.delete(key);
|
|
119
|
+
request.onsuccess = () => resolve();
|
|
120
|
+
request.onerror = () => resolve();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Purge all stale entries (older than MAX_AGE_MS). */
|
|
124
|
+
async purgeStale() {
|
|
125
|
+
if (!this.db) return 0;
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
128
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
129
|
+
const store = tx.objectStore(STORE_NAME);
|
|
130
|
+
const index = store.index("updated_at");
|
|
131
|
+
const range = IDBKeyRange.upperBound(cutoff);
|
|
132
|
+
const request = index.openCursor(range);
|
|
133
|
+
let count = 0;
|
|
134
|
+
request.onsuccess = () => {
|
|
135
|
+
const cursor = request.result;
|
|
136
|
+
if (cursor) {
|
|
137
|
+
cursor.delete();
|
|
138
|
+
count++;
|
|
139
|
+
cursor.continue();
|
|
140
|
+
} else {
|
|
141
|
+
resolve(count);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
request.onerror = () => resolve(0);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Close the database connection. */
|
|
148
|
+
close() {
|
|
149
|
+
if (this.db) {
|
|
150
|
+
this.db.close();
|
|
151
|
+
this.db = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
UploadResumeStore
|
|
158
|
+
};
|