@sentroy-co/client-sdk 2.6.3 → 2.8.0
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/AGENTS.md +791 -0
- package/README.md +70 -622
- package/dist/vault/index.d.ts +73 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +169 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/react.d.ts +27 -0
- package/dist/vault/react.d.ts.map +1 -0
- package/dist/vault/react.js +73 -0
- package/dist/vault/react.js.map +1 -0
- package/package.json +20 -5
- package/src/vault/index.ts +198 -0
- package/src/vault/react.tsx +125 -0
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<img src="https://sentroy.com/business/sentroy-logo-light.png" alt="Sentroy" width="240" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h3 align="center">Sentroy Client SDK
|
|
5
|
+
<h3 align="center">Sentroy Client SDK</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
TypeScript SDK
|
|
9
|
-
|
|
8
|
+
Official TypeScript SDK for the <a href="https://sentroy.com">Sentroy</a> business mail & storage platform.<br />
|
|
9
|
+
Send transactional and bulk email, manage domains and mailboxes, run an inbox, store and serve media — from one typed client.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -15,667 +15,115 @@
|
|
|
15
15
|
<a href="https://github.com/Sentroy-Co/client-sdk/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@sentroy-co/client-sdk.svg" alt="license" /></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://docs.sentroy.com"><strong>Documentation</strong></a>
|
|
20
|
+
·
|
|
21
|
+
<a href="https://docs.sentroy.com/mail">Mail</a>
|
|
22
|
+
·
|
|
23
|
+
<a href="https://docs.sentroy.com/storage">Storage</a>
|
|
24
|
+
·
|
|
25
|
+
<a href="https://docs.sentroy.com/react">React components</a>
|
|
26
|
+
·
|
|
27
|
+
<a href="https://status.sentroy.com">Status</a>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
18
30
|
---
|
|
19
31
|
|
|
20
|
-
##
|
|
32
|
+
## What's in the box
|
|
33
|
+
|
|
34
|
+
- **Mail** — verified domains, mailboxes, multi-language templates, IMAP-backed inbox, transactional and bulk send, suppressions, webhooks, audience lists, deliverability logs.
|
|
35
|
+
- **Storage** — isolated buckets, multipart uploads, image and media transformations, signed download URLs.
|
|
36
|
+
- **Env Vault** — runtime env variable management (`@sentroy-co/client-sdk/vault`) — change a value in the dashboard, your app picks it up on the next read; no rebuild.
|
|
37
|
+
- **React drop-ins** — `<MediaManager />`, `<MediaManagerTrigger />` and `<EnvProvider>` / `useEnv()` (`@sentroy-co/client-sdk/react` + `@sentroy-co/client-sdk/vault/react`).
|
|
38
|
+
- **One client, two backends** — point at `https://sentroy.com` for the hosted platform, or your own deployment for self-hosted. Same API, same types.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
21
41
|
|
|
22
42
|
```bash
|
|
23
43
|
npm install @sentroy-co/client-sdk
|
|
24
44
|
```
|
|
25
45
|
|
|
26
|
-
##
|
|
46
|
+
## First request
|
|
27
47
|
|
|
28
48
|
```ts
|
|
29
49
|
import { Sentroy } from "@sentroy-co/client-sdk"
|
|
30
50
|
|
|
31
51
|
const sentroy = new Sentroy({
|
|
32
|
-
baseUrl: "https://sentroy.com",
|
|
52
|
+
baseUrl: "https://sentroy.com", // or your self-hosted URL
|
|
33
53
|
companySlug: "my-company",
|
|
34
|
-
accessToken: "stk_...",
|
|
35
|
-
})
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
> Access tokens can be created from **Admin > Access Tokens** in the Sentroy dashboard.
|
|
39
|
-
|
|
40
|
-
## Usage
|
|
41
|
-
|
|
42
|
-
### Domains
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
// List all domains
|
|
46
|
-
const domains = await sentroy.domains.list()
|
|
47
|
-
|
|
48
|
-
// Get a single domain
|
|
49
|
-
const domain = await sentroy.domains.get("domain-id")
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Mailboxes
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
// List all mailbox accounts
|
|
56
|
-
const mailboxes = await sentroy.mailboxes.list()
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Templates
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
// List all templates
|
|
63
|
-
const templates = await sentroy.templates.list()
|
|
64
|
-
|
|
65
|
-
// Get a template by ID
|
|
66
|
-
const template = await sentroy.templates.get("template-id")
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Templates support multiple languages via `LocalizedString`. A field can be a plain string or an object keyed by language code:
|
|
70
|
-
|
|
71
|
-
```jsonc
|
|
72
|
-
// Example template response
|
|
73
|
-
{
|
|
74
|
-
"id": "b3f1a2c4-...",
|
|
75
|
-
"name": { "en": "Welcome Email", "tr": "Hosgeldin E-postasi" },
|
|
76
|
-
"subject": { "en": "Welcome, {{name}}!", "tr": "Hosgeldin, {{name}}!" },
|
|
77
|
-
"mjmlBody": { "en": "<mjml>...</mjml>", "tr": "<mjml>...</mjml>" },
|
|
78
|
-
"variables": ["name", "company"],
|
|
79
|
-
"domainId": "a1b2c3d4-...",
|
|
80
|
-
"domainName": "example.com",
|
|
81
|
-
"createdAt": "2026-01-15T10:30:00.000Z",
|
|
82
|
-
"updatedAt": "2026-04-10T14:22:00.000Z"
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Use the `variables` array to know which placeholders (`{{name}}`, `{{company}}`) the template expects.
|
|
87
|
-
|
|
88
|
-
### Inbox
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
// List messages
|
|
92
|
-
const messages = await sentroy.inbox.list({
|
|
93
|
-
mailbox: "info@example.com",
|
|
94
|
-
folder: "INBOX",
|
|
95
|
-
page: 1,
|
|
96
|
-
limit: 20,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Get a single message
|
|
100
|
-
const message = await sentroy.inbox.get(1234, {
|
|
101
|
-
mailbox: "info@example.com",
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// List IMAP folders
|
|
105
|
-
const folders = await sentroy.inbox.listFolders("info@example.com")
|
|
106
|
-
|
|
107
|
-
// Get a thread by subject
|
|
108
|
-
const thread = await sentroy.inbox.getThread("Re: Project update", "info@example.com")
|
|
109
|
-
|
|
110
|
-
// Mark as read / unread
|
|
111
|
-
await sentroy.inbox.markAsRead(1234, { mailbox: "info@example.com" })
|
|
112
|
-
await sentroy.inbox.markAsUnread(1234, { mailbox: "info@example.com" })
|
|
113
|
-
|
|
114
|
-
// Move message
|
|
115
|
-
await sentroy.inbox.move(1234, "Trash", {
|
|
116
|
-
from: "INBOX",
|
|
117
|
-
mailbox: "info@example.com",
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// Delete message
|
|
121
|
-
await sentroy.inbox.delete(1234, { mailbox: "info@example.com" })
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Audience
|
|
125
|
-
|
|
126
|
-
Manage contacts and audience lists from the SDK. Useful for building
|
|
127
|
-
your own newsletter signup form, syncing customers from another system,
|
|
128
|
-
or assembling segments for a campaign.
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// List + paginate contacts (filter by status / tags)
|
|
132
|
-
const { contacts, total, page, limit } = await sentroy.audience.contacts.list({
|
|
133
|
-
page: 1,
|
|
134
|
-
limit: 50,
|
|
135
|
-
status: "active",
|
|
136
|
-
tags: ["customer", "vip"],
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Email-prefix autocomplete (capped at 10 server-side)
|
|
140
|
-
const matches = await sentroy.audience.contacts.search("alex@")
|
|
141
|
-
|
|
142
|
-
// Create a contact
|
|
143
|
-
const contact = await sentroy.audience.contacts.create({
|
|
144
|
-
email: "user@example.com",
|
|
145
|
-
name: "Jane Doe",
|
|
146
|
-
tags: ["beta-tester"],
|
|
147
|
-
metadata: { signupSource: "landing-2026-q2" },
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
// Patch — pass any subset of fields. Use `status` to mark unsubscribed.
|
|
151
|
-
await sentroy.audience.contacts.update(contact.id, { tags: ["customer"] })
|
|
152
|
-
|
|
153
|
-
// Soft-delete (sets status: "unsubscribed" — record is preserved)
|
|
154
|
-
await sentroy.audience.contacts.delete(contact.id)
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Audience lists are simple groupings; a single contact can belong to many.
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
// CRUD audience lists
|
|
161
|
-
const lists = await sentroy.audience.lists.list()
|
|
162
|
-
const list = await sentroy.audience.lists.create({
|
|
163
|
-
name: "Newsletter — May 2026",
|
|
164
|
-
description: "Opt-ins from the homepage form",
|
|
165
|
-
})
|
|
166
|
-
await sentroy.audience.lists.delete(list.id)
|
|
167
|
-
|
|
168
|
-
// Membership — scoped to a single list id
|
|
169
|
-
const members = sentroy.audience.lists.members(list.id)
|
|
170
|
-
await members.add(contact.id)
|
|
171
|
-
const inList = await members.list()
|
|
172
|
-
await members.remove(contact.id)
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Suppressions
|
|
176
|
-
|
|
177
|
-
Suppressed addresses are skipped at send time. Bounces and complaints
|
|
178
|
-
are added automatically; the API is for manually honoring off-platform
|
|
179
|
-
opt-outs or removing a stale entry.
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
const suppressions = await sentroy.suppressions.list({
|
|
183
|
-
domainId: "domain-id",
|
|
184
|
-
reason: "complaint",
|
|
185
|
-
page: 1,
|
|
186
|
-
limit: 50,
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const added = await sentroy.suppressions.add({
|
|
190
|
-
email: "leaving@example.com",
|
|
191
|
-
domainId: "domain-id",
|
|
192
|
-
reason: "manual",
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
await sentroy.suppressions.remove(added.id)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### Webhooks
|
|
199
|
-
|
|
200
|
-
Subscribe to delivery events on a per-domain basis. The `secret`
|
|
201
|
-
returned at create time signs every delivery — store it and verify the
|
|
202
|
-
HMAC on your endpoint.
|
|
203
|
-
|
|
204
|
-
```ts
|
|
205
|
-
const webhook = await sentroy.webhooks.create({
|
|
206
|
-
url: "https://example.com/webhooks/sentroy",
|
|
207
|
-
events: ["sent", "bounced", "opened", "clicked", "unsubscribed"],
|
|
208
|
-
domainId: "domain-id",
|
|
209
|
-
})
|
|
210
|
-
console.log(webhook.secret) // Returned ONCE — store it now
|
|
211
|
-
|
|
212
|
-
const all = await sentroy.webhooks.list() // every webhook
|
|
213
|
-
const scoped = await sentroy.webhooks.list("domain-id")
|
|
214
|
-
|
|
215
|
-
await sentroy.webhooks.update(webhook.id, { active: false })
|
|
216
|
-
await sentroy.webhooks.delete(webhook.id)
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Logs
|
|
220
|
-
|
|
221
|
-
Query the mail log to debug delivery issues, surface per-message status
|
|
222
|
-
in your own UI, or build a customer-facing activity timeline.
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
const logs = await sentroy.logs.list({
|
|
226
|
-
status: "bounced",
|
|
227
|
-
domainId: "domain-id",
|
|
228
|
-
from: "2026-05-01T00:00:00Z",
|
|
229
|
-
to: "2026-05-31T23:59:59Z",
|
|
230
|
-
page: 1,
|
|
231
|
-
limit: 100,
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
const log = await sentroy.logs.get(logs[0].id)
|
|
235
|
-
console.log(log.openedAt, log.clickedAt) // tracking timestamps if enabled
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Send Email
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
// Send with a template (uses default language)
|
|
242
|
-
const result = await sentroy.send.email({
|
|
243
|
-
to: "user@example.com",
|
|
244
|
-
from: "info@example.com",
|
|
245
|
-
subject: "Welcome!",
|
|
246
|
-
domainId: "domain-id",
|
|
247
|
-
templateId: "template-id",
|
|
248
|
-
variables: {
|
|
249
|
-
name: "John",
|
|
250
|
-
company: "Acme",
|
|
251
|
-
},
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// Send with a specific language
|
|
255
|
-
const result = await sentroy.send.email({
|
|
256
|
-
to: "user@example.com",
|
|
257
|
-
from: "info@example.com",
|
|
258
|
-
subject: "Hosgeldin!",
|
|
259
|
-
domainId: "domain-id",
|
|
260
|
-
templateId: "template-id",
|
|
261
|
-
lang: "tr",
|
|
262
|
-
variables: { name: "Ahmet" },
|
|
54
|
+
accessToken: "stk_...", // Dashboard → Admin → Access Tokens
|
|
263
55
|
})
|
|
264
56
|
|
|
265
|
-
// Send
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
subject: "
|
|
270
|
-
|
|
271
|
-
html: "<h1>Hello World</h1>",
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
// Send with attachments
|
|
275
|
-
const result = await sentroy.send.email({
|
|
276
|
-
to: "user@example.com",
|
|
277
|
-
from: "info@example.com",
|
|
278
|
-
subject: "Invoice",
|
|
279
|
-
domainId: "domain-id",
|
|
280
|
-
html: "<p>Please find your invoice attached.</p>",
|
|
281
|
-
attachments: [
|
|
282
|
-
{
|
|
283
|
-
filename: "invoice.pdf",
|
|
284
|
-
content: base64String,
|
|
285
|
-
contentType: "application/pdf",
|
|
286
|
-
},
|
|
287
|
-
],
|
|
57
|
+
// Send your first transactional email
|
|
58
|
+
await sentroy.send.email({
|
|
59
|
+
from: "noreply@yourdomain.com",
|
|
60
|
+
to: ["customer@example.com"],
|
|
61
|
+
subject: "Welcome to Acme",
|
|
62
|
+
html: "<p>Glad you're here.</p>",
|
|
288
63
|
})
|
|
289
64
|
```
|
|
290
65
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
Storage is organized into **buckets** — isolated containers with their own
|
|
294
|
-
visibility (public vs private) and usage counters.
|
|
66
|
+
## Upload a file
|
|
295
67
|
|
|
296
68
|
```ts
|
|
297
|
-
|
|
298
|
-
const buckets = await sentroy.buckets.list()
|
|
69
|
+
const file = new File([blob], "invoice.pdf", { type: "application/pdf" })
|
|
299
70
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// Create a bucket (slug auto-derived from name if omitted)
|
|
304
|
-
const created = await sentroy.buckets.create({
|
|
305
|
-
name: "User Uploads",
|
|
306
|
-
description: "Avatars and profile media",
|
|
307
|
-
isPublic: false,
|
|
71
|
+
const media = await sentroy.media.upload({
|
|
72
|
+
bucketSlug: "invoices",
|
|
73
|
+
file,
|
|
308
74
|
})
|
|
309
75
|
|
|
310
|
-
//
|
|
311
|
-
await sentroy.buckets.update("product-assets", { isPublic: true })
|
|
312
|
-
|
|
313
|
-
// Delete a bucket (409 if it has files; use force to purge everything)
|
|
314
|
-
await sentroy.buckets.delete("product-assets", { force: true })
|
|
76
|
+
console.log(media.url) // signed URL, served from the CDN
|
|
315
77
|
```
|
|
316
78
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
Upload, list, download, and delete files inside a bucket. The same token
|
|
320
|
-
that authorizes mail calls also authorizes storage calls.
|
|
321
|
-
|
|
322
|
-
```ts
|
|
323
|
-
// List files in a bucket
|
|
324
|
-
const { items, total } = await sentroy.media.list("product-assets", {
|
|
325
|
-
type: "image",
|
|
326
|
-
limit: 50,
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
// Get a single media record
|
|
330
|
-
const media = await sentroy.media.get("product-assets", mediaId)
|
|
331
|
-
|
|
332
|
-
// Upload — browser (File from <input>)
|
|
333
|
-
const input = document.querySelector<HTMLInputElement>("input[type=file]")!
|
|
334
|
-
const file = input.files![0]
|
|
335
|
-
const uploaded = await sentroy.media.upload("product-assets", {
|
|
336
|
-
body: file,
|
|
337
|
-
folder: "products",
|
|
338
|
-
tags: ["v1", "cover"],
|
|
339
|
-
})
|
|
340
|
-
console.log(uploaded.url) // Public URL from the CDN
|
|
341
|
-
|
|
342
|
-
// Upload — Node.js (Blob from fs)
|
|
343
|
-
import { openAsBlob } from "node:fs"
|
|
344
|
-
const blob = await openAsBlob("./photo.jpg")
|
|
345
|
-
const uploaded = await sentroy.media.upload("product-assets", {
|
|
346
|
-
body: blob,
|
|
347
|
-
filename: "photo.jpg",
|
|
348
|
-
isPublic: true,
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// Download — streams from the storage backend; works for both public
|
|
352
|
-
// and private buckets (auth-gated for private).
|
|
353
|
-
const blob = await sentroy.media.download("product-assets", mediaId)
|
|
354
|
-
// Variant: ask for a pre-generated thumbnail width (falls back to
|
|
355
|
-
// original if that size wasn't generated for this file).
|
|
356
|
-
const thumb = await sentroy.media.download("product-assets", mediaId, {
|
|
357
|
-
quality: 500,
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// Delete — removes S3 objects (original + thumbnails) + Media record
|
|
361
|
-
await sentroy.media.delete("product-assets", mediaId)
|
|
362
|
-
```
|
|
79
|
+
That's the smallest useful surface. Every other resource (`domains`, `mailboxes`, `templates`, `inbox`, `audience`, `webhooks`, `suppressions`, `logs`, `buckets`, `media`) follows the same `sentroy.<resource>.<verb>(...)` shape with full TypeScript types.
|
|
363
80
|
|
|
364
|
-
|
|
81
|
+
## Env Vault
|
|
365
82
|
|
|
366
|
-
|
|
367
|
-
sizes (`media.imageMeta.thumbnails`). Showing the original 4000-px JPG
|
|
368
|
-
in a 56-px avatar wastes bandwidth and slows render. Use these helpers
|
|
369
|
-
to pick the right URL for the display target:
|
|
83
|
+
Manage your env vars in the dashboard at [vault.sentroy.com](https://vault.sentroy.com), bootstrap your deploy with one token, and read values via a typed helper — no rebuild on change.
|
|
370
84
|
|
|
371
85
|
```ts
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
pickPresetThumbnailUrl,
|
|
375
|
-
THUMBNAIL_PRESETS,
|
|
376
|
-
} from "@sentroy-co/client-sdk"
|
|
377
|
-
|
|
378
|
-
// Manual target (px) — pass display size * 2 for retina
|
|
379
|
-
const avatarUrl = pickThumbnailUrl(media, 56 * 2)
|
|
380
|
-
|
|
381
|
-
// Semantic preset — avatar / card / preview / hero
|
|
382
|
-
const cardUrl = pickPresetThumbnailUrl(media, "card") // → ~500px
|
|
383
|
-
const previewUrl = pickPresetThumbnailUrl(media, "preview") // → ~960px
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
The helper picks the smallest thumbnail that still **covers** the
|
|
387
|
-
target (so you never upscale), then falls back through:
|
|
86
|
+
// server side
|
|
87
|
+
import { getEnv, getEnvOrThrow, preloadEnv } from "@sentroy-co/client-sdk/vault"
|
|
388
88
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
4. `media.url` / `media.downloadUrl` if no thumbnails exist
|
|
393
|
-
(non-image, or image upload before thumbnails were generated).
|
|
394
|
-
|
|
395
|
-
Returns `undefined` only when the media has no public URL at all.
|
|
396
|
-
|
|
397
|
-
| Preset | Target px | Use case |
|
|
398
|
-
|-------------|-----------|----------|
|
|
399
|
-
| `avatar` | 128 | Round chips, 28-64 px display @2x |
|
|
400
|
-
| `card` | 500 | Grid / list cards, 200-300 px |
|
|
401
|
-
| `preview` | 960 | Modal / detail view |
|
|
402
|
-
| `hero` | 1600 | Full-bleed hero, edge cases |
|
|
403
|
-
|
|
404
|
-
## Error Handling
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
import { Sentroy, SentroyError } from "@sentroy-co/client-sdk"
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
await sentroy.send.email({ ... })
|
|
411
|
-
} catch (err) {
|
|
412
|
-
if (err instanceof SentroyError) {
|
|
413
|
-
console.error(err.statusCode) // 401, 403, 500, etc.
|
|
414
|
-
console.error(err.message) // Human-readable error
|
|
415
|
-
}
|
|
416
|
-
}
|
|
89
|
+
await preloadEnv() // optional fail-fast at boot
|
|
90
|
+
const dbUrl = await getEnv("DATABASE_URL")
|
|
91
|
+
const turnstile = await getEnvOrThrow("BETTER_AUTH_TURNSTILE_SECRET")
|
|
417
92
|
```
|
|
418
93
|
|
|
419
|
-
## Configuration
|
|
420
|
-
|
|
421
|
-
| Option | Type | Required | Description |
|
|
422
|
-
|--------|------|----------|-------------|
|
|
423
|
-
| `baseUrl` | `string` | Yes | Sentroy instance URL (e.g. `https://sentroy.com`) |
|
|
424
|
-
| `companySlug` | `string` | Yes | Your company slug |
|
|
425
|
-
| `accessToken` | `string` | Yes | Access token (`stk_...`) |
|
|
426
|
-
| `timeout` | `number` | No | Request timeout in ms (default: `30000`) |
|
|
427
|
-
|
|
428
|
-
## React components (`@sentroy-co/client-sdk/react`)
|
|
429
|
-
|
|
430
|
-
Optional subpath. Only loaded if you import it; React + react-dom are
|
|
431
|
-
declared as **optional peer dependencies** so server-only consumers
|
|
432
|
-
don't need to install them.
|
|
433
|
-
|
|
434
|
-
```bash
|
|
435
|
-
npm install react react-dom
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
### `MediaManager`
|
|
439
|
-
|
|
440
|
-
Drop-in storage browser/uploader for end-user apps. Talks to the same
|
|
441
|
-
Sentroy client you already use; renders Tailwind classes (host app's
|
|
442
|
-
Tailwind setup is reused — the package ships no styles).
|
|
443
|
-
|
|
444
94
|
```tsx
|
|
445
|
-
|
|
95
|
+
// React: SSR-injected provider + hook (no FOUC)
|
|
96
|
+
import { getPublicEnvs } from "@sentroy-co/client-sdk/vault"
|
|
97
|
+
import { EnvProvider, useEnv } from "@sentroy-co/client-sdk/vault/react"
|
|
446
98
|
|
|
447
|
-
|
|
448
|
-
|
|
99
|
+
// app/layout.tsx (server)
|
|
100
|
+
const envs = await getPublicEnvs()
|
|
101
|
+
return <EnvProvider envs={envs}>{children}</EnvProvider>
|
|
449
102
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
companySlug: "my-company",
|
|
453
|
-
accessToken: "stk_...",
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
export default function Page() {
|
|
457
|
-
return (
|
|
458
|
-
<MediaManager
|
|
459
|
-
client={client}
|
|
460
|
-
multiple
|
|
461
|
-
accept="image/*"
|
|
462
|
-
onChange={(selected) => console.log(selected)}
|
|
463
|
-
onSelect={(selected) => console.log("confirmed:", selected)}
|
|
464
|
-
/>
|
|
465
|
-
)
|
|
466
|
-
}
|
|
103
|
+
// any "use client" component
|
|
104
|
+
const siteKey = useEnv("TURNSTILE_SITE_KEY")
|
|
467
105
|
```
|
|
468
106
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
- Bucket selector (auto-picks first if `bucketSlug` not provided)
|
|
472
|
-
- Search (filename) + file-type filter (image / video / audio / pdf / doc / archive / code)
|
|
473
|
-
- Upload via button **and** drag-and-drop
|
|
474
|
-
- Single or multi selection (`multiple` prop)
|
|
475
|
-
- `initialValue` accepts `Media[]` or `string[]` (id list) — pre-selected
|
|
476
|
-
on mount, fires `onChange` immediately so parent state stays in sync
|
|
477
|
-
- Press `Space` while a card is selected → opens it in fullscreen
|
|
478
|
-
**Lightbox** (image / video / audio render natively, others get a
|
|
479
|
-
download fallback). `Esc` closes, `←/→` step through siblings
|
|
480
|
-
- Detail pane on the right (large screens) — preview, metadata,
|
|
481
|
-
delete, "Use selection" CTA when `onSelect` provided
|
|
482
|
-
|
|
483
|
-
#### Props
|
|
484
|
-
|
|
485
|
-
| Prop | Type | Required | Description |
|
|
486
|
-
|----------------------|-------------------------------------------------------|:-:|:--|
|
|
487
|
-
| `client` | `Sentroy` | Yes | The configured client instance |
|
|
488
|
-
| `bucketSlug` | `string` | | Initial bucket; default = first one in the list |
|
|
489
|
-
| `multiple` | `boolean` | | Allow multi-selection. Default `false` |
|
|
490
|
-
| `maxItems` | `number` | | Cap for multi-mode. New selections are silently blocked once reached. Ignored when `multiple=false` |
|
|
491
|
-
| `accept` | `string` | | File type filter — applies to upload **and** the grid. Same syntax as `<input accept>`: `"image/*"`, `"image/png,image/jpeg"`, `".pdf,.docx"`, comma-separated combos |
|
|
492
|
-
| `initialValue` | `Array<Media \| string>` | | Pre-selected items (objects or ids) |
|
|
493
|
-
| `onChange` | `(selected: Media[]) => void` | | Fires on every selection change |
|
|
494
|
-
| `onSelect` | `(selected: Media[]) => void` | | Fires on confirm — picker dialogs use this |
|
|
495
|
-
| `bucketFilter` | `(b: Bucket) => boolean` | | Filter the bucket dropdown — hide system buckets |
|
|
496
|
-
| `showDetailsPane` | `boolean` | | Default `true` |
|
|
497
|
-
| `showBucketSelector` | `boolean` | | Default `true` |
|
|
498
|
-
| `className` | `string` | | Root wrapper class |
|
|
499
|
-
| `classNames` | `MediaManagerClassNames` | | Per-region class overrides (see theming) |
|
|
500
|
-
|
|
501
|
-
#### Theming
|
|
502
|
-
|
|
503
|
-
The component uses Tailwind utility classes that consume your design
|
|
504
|
-
tokens (`bg-background`, `text-foreground`, `border-border`,
|
|
505
|
-
`text-muted-foreground`, `bg-muted`, etc.). Drop-in usage in any
|
|
506
|
-
shadcn-style codebase needs no extra setup.
|
|
507
|
-
|
|
508
|
-
For finer control, override individual sections via `classNames`:
|
|
107
|
+
Bootstrap is a single env: `SENTROY_ENV_API_KEY`. Public/private split is enforced server-side — the React hook only ever sees `public: true` variables. Full reference at [docs.sentroy.com/env-vault](https://docs.sentroy.com/env-vault).
|
|
509
108
|
|
|
510
|
-
|
|
511
|
-
<MediaManager
|
|
512
|
-
client={client}
|
|
513
|
-
className="h-[600px] rounded-2xl border-purple-200"
|
|
514
|
-
classNames={{
|
|
515
|
-
toolbar: "bg-purple-50",
|
|
516
|
-
uploadButton: "bg-purple-600 text-white",
|
|
517
|
-
cardSelected: "ring-purple-400 border-purple-400",
|
|
518
|
-
grid: "sm:grid-cols-2 md:grid-cols-3", // override grid density
|
|
519
|
-
}}
|
|
520
|
-
/>
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
Available keys: `root`, `toolbar`, `searchInput`, `filterSelect`,
|
|
524
|
-
`uploadButton`, `bucketSelect`, `grid`, `card`, `cardSelected`,
|
|
525
|
-
`thumbnail`, `cardMeta`, `empty`, `details`, `dropZoneOverlay`.
|
|
109
|
+
## Self-hosted vs hosted
|
|
526
110
|
|
|
527
|
-
|
|
528
|
-
one place — every Tailwind utility resolves through your `globals.css`.
|
|
111
|
+
The SDK is identical in both modes. Only `baseUrl` changes:
|
|
529
112
|
|
|
530
|
-
|
|
113
|
+
| Mode | `baseUrl` |
|
|
114
|
+
|---|---|
|
|
115
|
+
| Sentroy Cloud (hosted) | `https://sentroy.com` |
|
|
116
|
+
| Self-hosted | `https://your-sentroy-host` |
|
|
531
117
|
|
|
532
|
-
|
|
533
|
-
the user clicks the `trigger`, a portal-rendered modal opens with
|
|
534
|
-
`MediaManager` inside, and `onSelect` fires with the confirmed selection.
|
|
118
|
+
Pick the deployment that fits your compliance, latency and cost requirements. Migrate either direction without changing application code.
|
|
535
119
|
|
|
536
|
-
|
|
537
|
-
profile/settings page — you just want a "Change avatar" button (or even
|
|
538
|
-
a clickable avatar thumbnail) that pops the picker on demand.
|
|
120
|
+
## Documentation
|
|
539
121
|
|
|
540
|
-
|
|
541
|
-
"use client"
|
|
542
|
-
|
|
543
|
-
import { Sentroy } from "@sentroy-co/client-sdk"
|
|
544
|
-
import { MediaManagerTrigger } from "@sentroy-co/client-sdk/react"
|
|
122
|
+
Full reference, interactive examples, and multi-language code samples (TypeScript, Go, Python, PHP, cURL) live at:
|
|
545
123
|
|
|
546
|
-
|
|
547
|
-
baseUrl: "https://sentroy.com",
|
|
548
|
-
companySlug: "my-company",
|
|
549
|
-
accessToken: "stk_...",
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
export function AvatarPicker({
|
|
553
|
-
current,
|
|
554
|
-
onChange,
|
|
555
|
-
}: {
|
|
556
|
-
current: string | null
|
|
557
|
-
onChange: (url: string) => void
|
|
558
|
-
}) {
|
|
559
|
-
return (
|
|
560
|
-
<MediaManagerTrigger
|
|
561
|
-
client={client}
|
|
562
|
-
maxItems={1}
|
|
563
|
-
accept="image/*"
|
|
564
|
-
title="Choose your avatar"
|
|
565
|
-
description="Pick an existing image or upload a new one."
|
|
566
|
-
trigger={
|
|
567
|
-
<button className="rounded-full ring-2 ring-border hover:ring-primary">
|
|
568
|
-
{current ? (
|
|
569
|
-
<img src={current} alt="" className="size-10 rounded-full" />
|
|
570
|
-
) : (
|
|
571
|
-
<span className="grid size-10 place-items-center rounded-full bg-muted text-xs">
|
|
572
|
-
?
|
|
573
|
-
</span>
|
|
574
|
-
)}
|
|
575
|
-
</button>
|
|
576
|
-
}
|
|
577
|
-
onSelect={(media) => {
|
|
578
|
-
if (media[0]?.url) onChange(media[0].url)
|
|
579
|
-
}}
|
|
580
|
-
/>
|
|
581
|
-
)
|
|
582
|
-
}
|
|
583
|
-
```
|
|
124
|
+
**[docs.sentroy.com](https://docs.sentroy.com)**
|
|
584
125
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
```tsx
|
|
588
|
-
<MediaManagerTrigger
|
|
589
|
-
client={client}
|
|
590
|
-
maxItems={5}
|
|
591
|
-
accept="image/*,video/*"
|
|
592
|
-
trigger={<Button>Add gallery items</Button>}
|
|
593
|
-
onSelect={(media) => setGallery(media)}
|
|
594
|
-
/>
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
`maxItems > 1` automatically enables multi-mode. Once the user reaches
|
|
598
|
-
the cap, additional clicks on unselected cards are silently no-op'd —
|
|
599
|
-
they have to deselect something to swap.
|
|
600
|
-
|
|
601
|
-
#### Controlled mode
|
|
602
|
-
|
|
603
|
-
If you want the parent to drive open/close (e.g. opening from a context
|
|
604
|
-
menu), pass `open` + `onOpenChange`. The `trigger` is still rendered so
|
|
605
|
-
its click also opens the modal — to render only the modal, pass an empty
|
|
606
|
-
fragment for `trigger`.
|
|
607
|
-
|
|
608
|
-
```tsx
|
|
609
|
-
const [open, setOpen] = useState(false)
|
|
610
|
-
|
|
611
|
-
<MediaManagerTrigger
|
|
612
|
-
client={client}
|
|
613
|
-
open={open}
|
|
614
|
-
onOpenChange={setOpen}
|
|
615
|
-
trigger={<></>}
|
|
616
|
-
onSelect={(media) => { /* … */ }}
|
|
617
|
-
/>
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
#### Props
|
|
621
|
-
|
|
622
|
-
| Prop | Type | Required | Description |
|
|
623
|
-
|--------------------|-----------------------------------|:-:|:--|
|
|
624
|
-
| `client` | `Sentroy` | Yes | Same client you pass to `MediaManager` |
|
|
625
|
-
| `trigger` | `ReactNode` | Yes | The clickable element. Wrapped in `<span role="button">` with click + keyboard (Enter / Space) handlers |
|
|
626
|
-
| `onSelect` | `(selected: Media[]) => void` | Yes | Fires when user confirms; modal auto-closes |
|
|
627
|
-
| `maxItems` | `number` | | `1` = single (default), `>1` = multi up to cap |
|
|
628
|
-
| `accept` | `string` | | Same `<input accept>` syntax — applies to upload **and** grid filter |
|
|
629
|
-
| `title` | `string` | | Modal heading. Default `"Select media"` |
|
|
630
|
-
| `description` | `string` | | Subheading under the title |
|
|
631
|
-
| `open` | `boolean` | | Controlled open state |
|
|
632
|
-
| `onOpenChange` | `(open: boolean) => void` | | Controlled change handler |
|
|
633
|
-
| `disabled` | `boolean` | | Trigger ignores clicks; visual disabled state |
|
|
634
|
-
| `confirmLabel` | `string` | | Default `"Use selection"` |
|
|
635
|
-
| `cancelLabel` | `string` | | Default `"Cancel"` |
|
|
636
|
-
| `modalClassName` | `string` | | Class on the modal panel |
|
|
637
|
-
| `triggerClassName` | `string` | | Class on the trigger wrapper span |
|
|
638
|
-
| … | rest of `MediaManagerProps` | | `bucketSlug`, `bucketFilter`, `showDetailsPane`, `classNames`, etc. forwarded to the inner `MediaManager` |
|
|
639
|
-
|
|
640
|
-
The modal renders into `document.body` via `react-dom` portal, so it
|
|
641
|
-
escapes parent `overflow:hidden` / transform stacking contexts. `Esc`
|
|
642
|
-
closes; backdrop click closes; body scroll is locked while open.
|
|
643
|
-
|
|
644
|
-
#### `Lightbox` (standalone)
|
|
645
|
-
|
|
646
|
-
Exported separately so you can use it outside `MediaManager` (e.g. in
|
|
647
|
-
a feed view):
|
|
648
|
-
|
|
649
|
-
```tsx
|
|
650
|
-
import { Lightbox } from "@sentroy-co/client-sdk/react"
|
|
651
|
-
|
|
652
|
-
const [active, setActive] = useState<Media | null>(null)
|
|
653
|
-
|
|
654
|
-
return (
|
|
655
|
-
<>
|
|
656
|
-
{/* …trigger… */}
|
|
657
|
-
{active && (
|
|
658
|
-
<Lightbox media={active} onClose={() => setActive(null)} />
|
|
659
|
-
)}
|
|
660
|
-
</>
|
|
661
|
-
)
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Image / video / audio rendered inline; everything else gets a download
|
|
665
|
-
button. `Esc` closes, optional `onPrev` / `onNext` add ←/→ navigation.
|
|
666
|
-
|
|
667
|
-
#### Helpers
|
|
668
|
-
|
|
669
|
-
```ts
|
|
670
|
-
import {
|
|
671
|
-
cn, // tiny class joiner
|
|
672
|
-
formatBytes, // 1234 → "1.21 KB"
|
|
673
|
-
detectKind, // image | video | audio | pdf | doc | archive | code | other
|
|
674
|
-
matchAccept, // matchAccept(file, "image/*,.pdf") → boolean
|
|
675
|
-
KIND_LABELS,
|
|
676
|
-
type MediaKind,
|
|
677
|
-
} from "@sentroy-co/client-sdk/react"
|
|
678
|
-
```
|
|
126
|
+
Sections: [Quickstart](https://docs.sentroy.com) · [Mail](https://docs.sentroy.com/mail) · [Storage](https://docs.sentroy.com/storage) · [React](https://docs.sentroy.com/react) · [Tools](https://docs.sentroy.com/tools)
|
|
679
127
|
|
|
680
128
|
## Requirements
|
|
681
129
|
|
|
@@ -683,12 +131,12 @@ import {
|
|
|
683
131
|
- React 18+ (only if you import from `/react`)
|
|
684
132
|
- Tailwind CSS in the host app (only for React components)
|
|
685
133
|
|
|
686
|
-
##
|
|
134
|
+
## For AI agents
|
|
687
135
|
|
|
688
|
-
|
|
136
|
+
A single-file, comprehensive reference covering every endpoint, parameter and response shape lives at [`AGENTS.md`](AGENTS.md). Drop the raw URL into a context window:
|
|
689
137
|
|
|
690
138
|
```
|
|
691
|
-
https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/typescript/
|
|
139
|
+
https://raw.githubusercontent.com/Sentroy-Co/client-sdk/refs/heads/main/typescript/AGENTS.md
|
|
692
140
|
```
|
|
693
141
|
|
|
694
142
|
## License
|