@sentroy-co/client-sdk 2.13.9 → 2.15.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.
Files changed (49) hide show
  1. package/README.md +53 -5
  2. package/dist/auth/client.d.ts.map +1 -1
  3. package/dist/auth/client.js +8 -1
  4. package/dist/auth/client.js.map +1 -1
  5. package/dist/auth/react/index.js +1 -1
  6. package/dist/auth/react/index.js.map +1 -1
  7. package/dist/auth/react-native/index.d.ts +93 -0
  8. package/dist/auth/react-native/index.d.ts.map +1 -0
  9. package/dist/auth/react-native/index.js +106 -0
  10. package/dist/auth/react-native/index.js.map +1 -0
  11. package/dist/cli/ai.d.ts +35 -0
  12. package/dist/cli/ai.d.ts.map +1 -0
  13. package/dist/cli/ai.js +399 -0
  14. package/dist/cli/ai.js.map +1 -0
  15. package/dist/cli/args.d.ts +62 -0
  16. package/dist/cli/args.d.ts.map +1 -0
  17. package/dist/cli/args.js +199 -0
  18. package/dist/cli/args.js.map +1 -0
  19. package/dist/cli/env.d.ts.map +1 -1
  20. package/dist/cli/env.js +8 -2
  21. package/dist/cli/env.js.map +1 -1
  22. package/dist/cli/format.d.ts +37 -0
  23. package/dist/cli/format.d.ts.map +1 -0
  24. package/dist/cli/format.js +129 -0
  25. package/dist/cli/format.js.map +1 -0
  26. package/dist/cli/index.d.ts +8 -2
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +128 -25
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/mail.d.ts +25 -0
  31. package/dist/cli/mail.d.ts.map +1 -0
  32. package/dist/cli/mail.js +253 -0
  33. package/dist/cli/mail.js.map +1 -0
  34. package/dist/cli/storage.d.ts +28 -0
  35. package/dist/cli/storage.d.ts.map +1 -0
  36. package/dist/cli/storage.js +189 -0
  37. package/dist/cli/storage.js.map +1 -0
  38. package/package.json +9 -2
  39. package/skill/SKILL.md +577 -0
  40. package/src/auth/client.ts +8 -1
  41. package/src/auth/react/index.tsx +1 -1
  42. package/src/auth/react-native/index.ts +157 -0
  43. package/src/cli/ai.ts +440 -0
  44. package/src/cli/args.ts +225 -0
  45. package/src/cli/env.ts +10 -2
  46. package/src/cli/format.ts +147 -0
  47. package/src/cli/index.ts +147 -25
  48. package/src/cli/mail.ts +363 -0
  49. package/src/cli/storage.ts +307 -0
package/skill/SKILL.md ADDED
@@ -0,0 +1,577 @@
1
+ ---
2
+ name: sentroy
3
+ version: 2.15.0
4
+ description: Use when working with the Sentroy platform SDK / REST API for mail (send, templates, domains, mailboxes, inbox, suppressions, logs), storage (buckets, media, multipart upload, CDN), env-vault (config / secrets), or auth-as-a-service (Auth Projects, signup/login/JWT). Covers auth modes (stk_ access token, aps_ Auth Project key), base URLs, common task recipes, error codes, gotchas, and the `sentroy` CLI.
5
+ ---
6
+
7
+ ## TL;DR
8
+
9
+ **Sentroy** is a unified developer platform: transactional **mail**, S3-backed **storage + CDN**, **env vault** (config + secrets sync), and **auth-as-a-service** (per-app end-user pools). Official SDKs in TypeScript, Python, PHP, and Go all wrap the same REST surface; a `sentroy` CLI ships with the TS package. This skill covers SDK + REST integration. It is **not** an end-user docs portal, dashboard UI guide, or billing reference.
10
+
11
+ ## Base URLs
12
+
13
+ The TypeScript SDK takes the **platform root** as `baseUrl` and rewrites internally — do not pass subdomains.
14
+
15
+ | Service | Production URL | Notes |
16
+ |---|---|---|
17
+ | Platform root (SDK `baseUrl`) | `https://sentroy.com` | SDK auto-routes `/api/mail/*` and `/api/storage/*` |
18
+ | Mail API | `https://mail.sentroy.com` | Direct REST consumers only |
19
+ | Storage API | `https://storage.sentroy.com` | Direct REST consumers only |
20
+ | CDN (public media) | `https://cdn.sentroy.com/f/<mediaId>[/<quality>]` | No auth, mediaId is unguessable |
21
+ | Auth Projects API | `https://auth.sentroy.com/api/v1/auth/<projectSlug>/...` | Separate `aps_` key |
22
+ | Docs | `https://docs.sentroy.com` | |
23
+
24
+ Self-hosted / staging: override `baseUrl` and the SDK will compose every path under it.
25
+
26
+ ## Authentication
27
+
28
+ Four auth modes exist. **Pick once per integration.**
29
+
30
+ | Mode | Header / mechanism | When to use |
31
+ |---|---|---|
32
+ | **Access token (`stk_`)** | `Authorization: Bearer stk_<48-hex>` | 99% of agent + SDK work. Company-scoped, permission-list scoped. |
33
+ | **Auth Project key (`aps_`)** | `Authorization: Bearer aps_<48-hex>` | Auth-as-a-Service public API only (`/api/v1/auth/<slug>/...`). |
34
+ | **Internal secret** | `x-internal-secret: <secret>` | Server-to-server inside the Sentroy infra. **Never** use from an agent. |
35
+ | **Session cookie** | better-auth cookie on `.sentroy.com` | Dashboard UI only. Not for SDK or CLI. |
36
+
37
+ **Decision tree:**
38
+
39
+ 1. Calling something under `/api/companies/<slug>/...`? → **stk_ token**.
40
+ 2. Calling `/api/v1/auth/<projectSlug>/{signup,login,...}` on behalf of an Auth Project? → **aps_ key**.
41
+ 3. Anything else (e.g. dashboard automation through the UI itself)? → out of scope for this skill.
42
+
43
+ **Creating an `stk_` token:** Dashboard → company → Settings → Access Tokens → "New". The plaintext is shown **once on create** — store it immediately. After that only `tokenPrefix` (first 12 chars) is visible. Pick the minimum permission set (see [Permission scopes](#permission-scopes)).
44
+
45
+ **Creating an `aps_` key:** Dashboard → Auth Projects → `<project>` → API Keys → "New". Same rules: plaintext on create only. These are **master keys** for the entire end-user pool — treat as a server secret. Never ship to a browser bundle.
46
+
47
+ ## Install & quick start
48
+
49
+ ### TypeScript
50
+
51
+ ```bash
52
+ npm install @sentroy-co/client-sdk
53
+ ```
54
+
55
+ ```ts
56
+ import { Sentroy } from "@sentroy-co/client-sdk";
57
+
58
+ const sentroy = new Sentroy({
59
+ baseUrl: "https://sentroy.com",
60
+ companySlug: "acme",
61
+ accessToken: process.env.SENTROY_API_KEY!, // stk_...
62
+ });
63
+
64
+ const domains = await sentroy.domains.list();
65
+ console.log(domains);
66
+ ```
67
+
68
+ ### React Native / Expo
69
+
70
+ The auth SDK works in Expo/React Native with two small additions:
71
+
72
+ 1. Plug in async storage (sessions persist across cold-starts):
73
+
74
+ ```ts
75
+ import AsyncStorage from "@react-native-async-storage/async-storage"
76
+ import { SentroyAuth } from "@sentroy-co/client-sdk/auth"
77
+ import { createAsyncStorageAdapter } from "@sentroy-co/client-sdk/auth/react-native"
78
+
79
+ export const auth = new SentroyAuth({
80
+ projectSlug: "acme",
81
+ apiKey: process.env.EXPO_PUBLIC_SENTROY_AUTH_KEY!,
82
+ storage: createAsyncStorageAdapter(AsyncStorage, { projectSlug: "acme" }),
83
+ })
84
+ ```
85
+
86
+ 2. Social login via expo-web-browser:
87
+
88
+ ```ts
89
+ import * as WebBrowser from "expo-web-browser"
90
+ import { openSocialAuthSession } from "@sentroy-co/client-sdk/auth/react-native"
91
+
92
+ const tokens = await openSocialAuthSession(WebBrowser, {
93
+ authorizeUrl: auth.socialAuthorizeUrl("google", {
94
+ redirectUri: "myapp://auth/callback",
95
+ }),
96
+ redirectUri: "myapp://auth/callback",
97
+ })
98
+ if (tokens) await auth.setSession(tokens)
99
+ ```
100
+
101
+ **Gotchas:** `SentroyAuthAdmin` is server-only (do not import in Expo bundles). Passkeys are web-only. `media.upload` from Expo `DocumentPicker` needs `{uri, name, type}` as `body`. Old RN (<0.71) needs an `atob` polyfill.
102
+
103
+ ### Python
104
+
105
+ > **Note:** Python/PHP/Go SDK packages are in development; today they map 1:1 to raw HTTP calls — see the cURL recipes.
106
+
107
+ ```bash
108
+ pip install sentroy
109
+ ```
110
+
111
+ ```python
112
+ from sentroy import Sentroy
113
+
114
+ sentroy = Sentroy(
115
+ base_url="https://sentroy.com",
116
+ company_slug="acme",
117
+ access_token=os.environ["SENTROY_API_KEY"],
118
+ )
119
+
120
+ print(sentroy.domains.list())
121
+ ```
122
+
123
+ ### PHP
124
+
125
+ ```bash
126
+ composer require sentroy/client-sdk
127
+ ```
128
+
129
+ ```php
130
+ use Sentroy\Sentroy;
131
+
132
+ $sentroy = new Sentroy([
133
+ 'baseUrl' => 'https://sentroy.com',
134
+ 'companySlug' => 'acme',
135
+ 'accessToken' => getenv('SENTROY_API_KEY'),
136
+ ]);
137
+
138
+ print_r($sentroy->domains->list());
139
+ ```
140
+
141
+ ### Go
142
+
143
+ ```bash
144
+ go get github.com/Sentroy-Co/client-sdk/go
145
+ ```
146
+
147
+ ```go
148
+ import "github.com/Sentroy-Co/client-sdk/go/sentroy"
149
+
150
+ client := sentroy.New(sentroy.Config{
151
+ BaseURL: "https://sentroy.com",
152
+ CompanySlug: "acme",
153
+ AccessToken: os.Getenv("SENTROY_API_KEY"),
154
+ })
155
+
156
+ domains, err := client.Domains.List(ctx)
157
+ ```
158
+
159
+ ### cURL
160
+
161
+ ```bash
162
+ curl -H "Authorization: Bearer $SENTROY_API_KEY" \
163
+ https://sentroy.com/api/companies/acme/domains
164
+ ```
165
+
166
+ ## Common task recipes
167
+
168
+ ### 1. Send a templated email
169
+
170
+ ```ts
171
+ const result = await sentroy.send.email({
172
+ domainId: "dom_abc", // REQUIRED — the verified sending domain
173
+ templateId: "tpl_welcome",
174
+ to: "alice@example.com",
175
+ from: "noreply@acme.com", // must belong to the verified domain above
176
+ variables: { firstName: "Alice", confirmUrl: "https://acme.com/c/abc" },
177
+ });
178
+ // → { jobId: "job_…", mailLogId: "log_…", status: "queued", scheduledAt?: "2026-…" }
179
+ ```
180
+
181
+ Common error: `400 "from address domain not verified"` — verify the domain first (recipe 3).
182
+
183
+ ### 2. Send a raw email (no template)
184
+
185
+ ```ts
186
+ await sentroy.send.email({
187
+ domainId: "dom_abc", // REQUIRED
188
+ to: "bob@example.com",
189
+ from: "alerts@acme.com",
190
+ subject: "Build #182 failed",
191
+ html: "<p>See log…</p>",
192
+ text: "See log…",
193
+ });
194
+ // → { jobId: "job_…", mailLogId: "log_…", status: "queued" }
195
+ ```
196
+
197
+ Common error: `403 send.execute` — the token lacks the `send.execute` permission.
198
+
199
+ ### 3. Create + verify a domain (raw HTTP)
200
+
201
+ The TS SDK currently only exposes `sentroy.domains.list()` and `sentroy.domains.get(id)`. Create + verify must go through raw HTTP today.
202
+
203
+ ```bash
204
+ # Create the domain — returns the DNS records you need to publish
205
+ curl -X POST \
206
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
207
+ -H "Content-Type: application/json" \
208
+ -d '{"name":"acme.com"}' \
209
+ https://sentroy.com/api/companies/acme/domains
210
+ # → { id: "dom_…", status: "pending", dnsRecords: [
211
+ # { type: "TXT", name: "@", value: "v=spf1 …" },
212
+ # { type: "CNAME", name: "s1._domainkey", value: "s1.dkim.…" },
213
+ # { type: "CNAME", name: "s2._domainkey", value: "s2.dkim.…" },
214
+ # { type: "TXT", name: "_dmarc", value: "v=DMARC1 …" },
215
+ # ]}
216
+
217
+ # After publishing DNS, trigger re-check:
218
+ curl -X POST \
219
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
220
+ https://sentroy.com/api/companies/acme/domains/dom_abc/verify
221
+ # → { status: "verified" | "pending" | "failed", checks: { spf, dkim, dmarc } }
222
+ ```
223
+
224
+ The DNS records returned cover SPF, two DKIM selectors, and DMARC — publish all four for full deliverability.
225
+
226
+ Common error: `status: "pending"` for up to 60 min as DNS propagates. Poll, don't loop tightly.
227
+
228
+ ### 4. Create a mailbox (raw HTTP)
229
+
230
+ The TS SDK currently only exposes `sentroy.mailboxes.list()`. Create goes through raw HTTP.
231
+
232
+ ```bash
233
+ curl -X POST \
234
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
235
+ -H "Content-Type: application/json" \
236
+ -d '{
237
+ "domainId": "dom_abc",
238
+ "localPart": "support",
239
+ "displayName": "Acme Support",
240
+ "password": "'"$(uuidgen)"'"
241
+ }' \
242
+ https://sentroy.com/api/companies/acme/mailboxes
243
+ # → { id: "mb_…", address: "support@acme.com" }
244
+ ```
245
+
246
+ Common error: `409 "mailbox already exists"` — local part collision on the domain.
247
+
248
+ ### 5. Upload a file to a bucket (single, public)
249
+
250
+ `bucketSlug` is the **first positional argument**; the field is `body` (Blob / Buffer / stream), and visibility is `isPublic: boolean`.
251
+
252
+ ```ts
253
+ const file = await fs.promises.readFile("./hero.jpg");
254
+ const media = await sentroy.media.upload("marketing", {
255
+ body: file,
256
+ filename: "hero.jpg",
257
+ contentType: "image/jpeg",
258
+ isPublic: true, // → served from cdn.sentroy.com/f/<id>
259
+ });
260
+ // → { id: "med_…", url: "https://cdn.sentroy.com/f/med_…", size: 482103 }
261
+ ```
262
+
263
+ Common error: `413 "file too big"` — single-shot upload limit applies; for very large files, see the note below.
264
+
265
+ > **Large files (>100MB):** upload via the Storage dashboard, which uses a 3-parallel multipart pool internally. Programmatic multipart from the SDK is roadmap.
266
+
267
+ ### 6. List media in a bucket (paginated)
268
+
269
+ `bucketSlug` is positional. Pagination is **offset-based** (`skip`/`limit`), not cursor-based.
270
+
271
+ ```ts
272
+ const page = await sentroy.media.list("marketing", {
273
+ limit: 50,
274
+ skip: 0, // offset; bump by `limit` for next page
275
+ type: "image", // image | video | audio | doc | other
276
+ folder: "/heroes",
277
+ q: "hero", // optional search
278
+ sort: "createdAt", // optional
279
+ dir: "desc", // optional
280
+ });
281
+ // → { items: [...], total: 137, limit: 50, skip: 0, sort?: "createdAt", dir?: "desc" }
282
+ ```
283
+
284
+ ### 7. Sign up an end-user via Auth Project (cURL)
285
+
286
+ ```bash
287
+ curl -X POST \
288
+ -H "Authorization: Bearer $SENTROY_APS_KEY" \
289
+ -H "Content-Type: application/json" \
290
+ -d '{"email":"alice@example.com","password":"hunter2","name":"Alice"}' \
291
+ https://auth.sentroy.com/api/v1/auth/my-app/signup
292
+ # → { user: {...}, accessToken: "<jwt>", refreshToken: "..." }
293
+ ```
294
+
295
+ Python/PHP/Go auth SDK subpaths are not yet shipped — use raw HTTP. JWTs are RS256 with project-specific keypairs; verify against `https://auth.sentroy.com/api/v1/auth/<slug>/jwks.json`.
296
+
297
+ ## Resource API surface
298
+
299
+ All paths are relative to `https://sentroy.com/api/companies/<slug>` unless noted.
300
+
301
+ ### Mail — domains
302
+
303
+ | Method | Path | Description | Permission |
304
+ |---|---|---|---|
305
+ | GET | `/domains` | List domains | `domains.view` |
306
+ | POST | `/domains` | Create + return DNS records | `domains.create` |
307
+ | GET | `/domains/{id}` | Detail + verification state | `domains.view` |
308
+ | POST | `/domains/{id}/verify` | Re-check DNS | `domains.edit` |
309
+ | DELETE | `/domains/{id}` | Remove | `domains.delete` |
310
+
311
+ ### Mail — mailboxes
312
+
313
+ | Method | Path | Description | Permission |
314
+ |---|---|---|---|
315
+ | GET | `/mailboxes` | List | `mailboxes.manage` |
316
+ | POST | `/mailboxes` | Create | `mailboxes.manage` |
317
+ | PATCH | `/mailboxes/{id}` | Rename / change password / quota | `mailboxes.manage` |
318
+ | DELETE | `/mailboxes/{id}` | Remove | `mailboxes.manage` |
319
+
320
+ ### Mail — templates
321
+
322
+ | Method | Path | Description | Permission |
323
+ |---|---|---|---|
324
+ | GET | `/templates` | List | `templates.manage` |
325
+ | POST | `/templates` | Create (name/subject/body accept LocalizedString) | `templates.manage` |
326
+ | GET | `/templates/{id}` | Detail | `templates.manage` |
327
+ | PATCH | `/templates/{id}` | Update | `templates.manage` |
328
+ | DELETE | `/templates/{id}` | Remove | `templates.manage` |
329
+
330
+ ### Mail — inbox
331
+
332
+ The `{uid}` path param is the **IMAP UID** (not a Mongo `_id`). The `mailbox` and `folder` query params are **load-bearing** — they identify which IMAP folder owns the UID and must be passed on every single-message call.
333
+
334
+ | Method | Path | Description | Permission |
335
+ |---|---|---|---|
336
+ | GET | `/inbox?mailbox=<addr>&folder=<inbox\|sent\|trash>&unread=<bool>` | List messages | `inbox.view` |
337
+ | GET | `/inbox/{uid}?mailbox=<addr>&folder=<folder>` | Message + parsed parts | `inbox.view` |
338
+ | POST | `/inbox/{uid}/read?mailbox=<addr>&folder=<folder>` | Mark read | `inbox.view` |
339
+ | DELETE | `/inbox/{uid}?mailbox=<addr>&folder=<folder>` | Trash | `inbox.view` |
340
+
341
+ ### Mail — send / suppressions / webhooks / logs / analytics
342
+
343
+ | Method | Path | Description | Permission |
344
+ |---|---|---|---|
345
+ | POST | `/send` | Send (template or raw)¹ | `send.execute` |
346
+ | GET | `/suppressions` | Bounced/complained addresses | `suppressions.manage` |
347
+ | POST | `/suppressions` | Add manually | `suppressions.manage` |
348
+ | DELETE | `/suppressions/{addr}` | Remove | `suppressions.manage` |
349
+ | GET | `/webhooks` | List endpoints | `webhooks.manage` |
350
+ | POST | `/webhooks` | Subscribe (`events: ["delivered","bounced",...]`) | `webhooks.manage` |
351
+ | GET | `/logs` | Send log (filter `status`, `domain`, `from`, `to`) | `logs.view` |
352
+ | GET | `/logs/{id}` | Message timeline | `logs.view` |
353
+ | GET | `/analytics` | Aggregate counts (param `days=7|30|90`) | `logs.view` |
354
+
355
+ ¹ TS SDK method `sentroy.send.email()` calls `POST /send` for ergonomics — pass either `templateId` + `variables` or `subject` + `html`/`text`, plus the required `domainId`.
356
+
357
+ ### Storage — buckets
358
+
359
+ | Method | Path | Description | Permission |
360
+ |---|---|---|---|
361
+ | GET | `/buckets` | List | `storage.view` |
362
+ | POST | `/buckets` | Create | `buckets.create` |
363
+ | GET | `/buckets/{slug}` | Detail | `storage.view` |
364
+ | PATCH | `/buckets/{slug}` | Rename / visibility | `buckets.edit` |
365
+ | DELETE | `/buckets/{slug}` | Force-delete cascade | `buckets.delete` |
366
+
367
+ ### Storage — media
368
+
369
+ Multipart upload is **not** part of the public REST API today — see Recipe 5 for the upload story. The list endpoint uses offset pagination (`skip`/`limit`), not cursor.
370
+
371
+ | Method | Path | Description | Permission |
372
+ |---|---|---|---|
373
+ | GET | `/buckets` | List buckets | `storage.view` |
374
+ | GET | `/buckets/{slug}` | Bucket detail | `storage.view` |
375
+ | GET | `/buckets/{slug}/media` | List (`limit`, `skip`, `type`, `folder`, `q`, `sort`, `dir`) | `storage.view` |
376
+ | GET | `/buckets/{slug}/media/{mediaId}` | Detail | `storage.view` |
377
+ | GET | `/buckets/{slug}/media/{mediaId}/download` | Authenticated download URL | `storage.view` |
378
+ | GET | `/usage` | Per-bucket usage stats | `storage.view` |
379
+ | GET | `/storage-quota` | Used + limit bytes (company-wide) | `storage.view` |
380
+
381
+ ### Env vault
382
+
383
+ Env vault uses **its own scoped token** (`Authorization: Bearer stk_env_<...>`), not the standard `stk_` access token or the permission engine. The three endpoints are token-scoped (no company in the path) — the token itself identifies the target vault project.
384
+
385
+ | Method | Path | Description |
386
+ |---|---|---|
387
+ | POST | `/api/env-vault/push` | Full sync up (CLI flag `--delete-missing` controls removal of vault keys absent locally) |
388
+ | POST | `/api/env-vault/fetch` | Full snapshot down (server-authoritative state) |
389
+ | GET | `/api/env-vault/public` | Browser-safe subset only (keys flagged public) |
390
+
391
+ ### Auth-as-a-Service (uses `aps_` key, host = `auth.sentroy.com`)
392
+
393
+ > **Not wrapped by the TS SDK.** The `Sentroy` class only knows mail/storage. Invoke these endpoints directly with `fetch` or cURL against the `auth.sentroy.com` host.
394
+
395
+ | Method | Path | Description |
396
+ |---|---|---|
397
+ | POST | `/api/v1/auth/{slug}/signup` | Create end-user |
398
+ | POST | `/api/v1/auth/{slug}/login` | Issue JWT |
399
+ | POST | `/api/v1/auth/{slug}/refresh` | Refresh JWT |
400
+ | POST | `/api/v1/auth/{slug}/logout` | Revoke session |
401
+ | GET | `/api/v1/auth/{slug}/userinfo` | Bearer-validated user |
402
+ | POST | `/api/v1/auth/{slug}/verify-email` | Confirm token |
403
+ | POST | `/api/v1/auth/{slug}/password-reset/request` | Send reset mail |
404
+ | POST | `/api/v1/auth/{slug}/password-reset/confirm` | Apply new password |
405
+ | GET | `/api/v1/auth/{slug}/jwks.json` | Public keys for JWT verify |
406
+
407
+ ## LocalizedString gotcha
408
+
409
+ Many mail-side string fields (template `name`, `subject`, `body`, status branding `tagline`, etc.) accept **either** a plain string **or** a `{tr, en}` object. Both forms are valid for the same endpoint.
410
+
411
+ ```ts
412
+ // Plain string — applied to all locales
413
+ await sentroy.templates.create({
414
+ name: "Welcome",
415
+ subject: "Welcome to Acme",
416
+ body: "<p>Hi {{firstName}}</p>",
417
+ });
418
+
419
+ // Localized — different copy per locale
420
+ await sentroy.templates.create({
421
+ name: { tr: "Hoş geldin", en: "Welcome" },
422
+ subject: { tr: "Acme'ye hoş geldin", en: "Welcome to Acme" },
423
+ body: { tr: "<p>Merhaba {{firstName}}</p>", en: "<p>Hi {{firstName}}</p>" },
424
+ });
425
+ ```
426
+
427
+ If a recipient's locale is missing the SDK falls back to `en` then to the first available key.
428
+
429
+ ## Permission scopes
430
+
431
+ Request the **minimum** scope. Wildcards exist but should be a last resort.
432
+
433
+ ```
434
+ domains.view domains.create domains.edit domains.delete domains.manage
435
+ mailboxes.manage
436
+ templates.manage
437
+ inbox.view
438
+ audience.manage
439
+ send.execute
440
+ logs.view
441
+ webhooks.manage
442
+ suppressions.manage
443
+ api-keys.manage
444
+ smtp.manage
445
+ members.manage
446
+ storage.view
447
+ buckets.create buckets.edit buckets.delete
448
+ media.upload media.delete media.reorder
449
+ ```
450
+
451
+ - **Wildcards:** `<resource>.manage` grants every action on that resource. Use only when the integration genuinely needs full CRUD.
452
+ - **Scoped (legacy):** `domains.domain:<id>` — all actions on one specific domain.
453
+ - **Scoped (granular):** `domains.domain:<id>:<action>` (`view|edit|delete|create`) — one action on one domain.
454
+ - **Mailbox-scoped inbox:** `inbox.mailbox:<email>` — read only one mailbox's mail.
455
+
456
+ **Dashboard-only scopes (not relevant to `stk_` REST callers):**
457
+ `oauth-clients.manage`, `auth-projects.manage` — these gate dashboard UI actions for managing OAuth Clients and Auth Projects. Auth Project public API access uses the per-project `aps_` key instead; OAuth provider endpoints are unauthenticated by design.
458
+
459
+ Owner / admin company members bypass scope checks; `member` role is granular.
460
+
461
+ ## Errors
462
+
463
+ Standard JSON envelope on failure — note there is **no** `success`, `code`, or `details` field:
464
+
465
+ ```json
466
+ { "data": null, "error": "Human-readable message" }
467
+ ```
468
+
469
+ On success the envelope is `{ "data": <payload>, "error": null }`. Branch on the HTTP status code and read `error` for the user-displayable string.
470
+
471
+ | Status | Meaning | Typical fix |
472
+ |---|---|---|
473
+ | 400 | Validation failed | Read `error`; fix payload |
474
+ | 401 | Missing / malformed / expired token | Re-issue `stk_` or `aps_` |
475
+ | 403 | Token lacks the required permission | Add scope in dashboard |
476
+ | 404 | Resource not in this company (or wrong slug) | Check `companySlug` and resource id |
477
+ | 409 | Conflict (duplicate slug, mailbox, etc.) | Use a different identifier |
478
+ | 413 | Payload too large | Compress or upload via dashboard |
479
+ | 422 | Semantic error (e.g. `from` domain unverified) | Resolve precondition |
480
+ | 429 | Rate-limited | Honor `Retry-After` header; back off |
481
+ | 500 / 502 / 503 | Server error | Retry with exponential backoff |
482
+
483
+ ## Rate limits
484
+
485
+ Per-token + per-company rate limits are enforced by the platform. `429` returns include a `Retry-After: <seconds>` header — honor it, then apply exponential backoff with jitter. Check your dashboard for plan-specific ceilings.
486
+
487
+ ## Gotchas & footguns
488
+
489
+ 1. **`stk_` plaintext is shown only on create.** It is irretrievable afterward — store it the moment you create one.
490
+ 2. **`tokenPrefix` (first 12 chars)** is the only identifier visible in lists / dashboard after creation. Use it to disambiguate tokens, not the full secret.
491
+ 3. **`baseUrl` = platform root**, never a subdomain. The SDK rewrites `/api/mail/*` → `mail.sentroy.com` and `/api/storage/*` → `storage.sentroy.com` for you.
492
+ 4. **Cross-subdomain cookie** works only in production on `.sentroy.com`. Local dev uses per-port cookies; expect to log in to each app separately.
493
+ 5. **Avatar / logo uploads** use the `DirectAvatarUpload` React helper, **not** `MediaManagerTrigger` — no bucket picker, just crop + POST.
494
+ 6. **Tailwind v4 + `MediaManager`:** add `@source "../node_modules/@sentroy-co/client-sdk/dist/react";` to `globals.css` or component classes will be tree-shaken and render unstyled.
495
+ 7. **`CropDialog` CSS:** import `"@sentroy-co/client-sdk/react/crop/styles.css"` exactly once in the root layout — required for `react-mobile-cropper` baseline styles.
496
+ 8. **`<SelectValue>` is forbidden for slug/enum/id values.** Render the human label manually inside `<SelectTrigger>` — the raw value would otherwise leak to the UI.
497
+ 9. **`aps_` Auth Project keys are master keys.** Never expose to a browser bundle or a mobile binary. A browser-safe public-key tier is on the roadmap.
498
+ 10. **Storage quota:** preflight large uploads with `GET /storage-quota` — `413` after upload start wastes bytes against your budget.
499
+ 11. **Domain verification propagation:** DNS publishes in 5–60 min. Poll `/domains/{id}` (or call `verify`) every 30–60 s, not in a tight loop.
500
+
501
+ ## CLI
502
+
503
+ The TS package ships a `sentroy` binary. Install once globally or use via `npx`:
504
+
505
+ ```bash
506
+ npm install -g @sentroy-co/client-sdk # global
507
+ npx sentroy <command> # ad-hoc
508
+ ```
509
+
510
+ **Auth via env (preferred):**
511
+
512
+ ```bash
513
+ export SENTROY_API_KEY=stk_…
514
+ export SENTROY_COMPANY_SLUG=acme
515
+ # Env vault subgroup uses its own scoped token:
516
+ export SENTROY_ENV_API_KEY=stk_env_…
517
+ ```
518
+
519
+ Or per-invocation flags: `--token`, `--company-slug`, `--url` (defaults to `https://sentroy.com`).
520
+
521
+ **Global flags:** `--token`, `--url`, `--company-slug`, `--output=json|table` (default `table`). Every list/get command supports `--output=json` for scripting / piping into `jq`.
522
+
523
+ **Commands:**
524
+
525
+ ```bash
526
+ # Env vault sync
527
+ sentroy env push # local .env → vault
528
+ sentroy env pull # vault → local .env
529
+ sentroy env list # show all keys
530
+ sentroy env diff # local vs. vault
531
+
532
+ # Mail
533
+ sentroy mail templates list
534
+ sentroy mail templates get <id>
535
+ sentroy mail domains list
536
+ sentroy mail mailboxes list
537
+ sentroy mail inbox list [--mailbox=<addr>] [--folder=inbox|sent|trash] [--unread]
538
+ sentroy mail suppressions list
539
+ sentroy mail logs list [--status=delivered|bounced|deferred] [--domain=<name>] [--from=<iso>] [--to=<iso>]
540
+ sentroy mail logs get <id>
541
+ sentroy mail webhooks list
542
+ sentroy mail analytics [--days=7|30|90]
543
+
544
+ # Storage
545
+ sentroy storage buckets list
546
+ sentroy storage buckets get <bucketSlug>
547
+ sentroy storage media list <bucketSlug> [--type=image|video|audio|doc|other] [--folder=<path>] [--q=<query>]
548
+ sentroy storage media get <bucketSlug> <mediaId>
549
+ sentroy storage usage
550
+ sentroy storage quota # company-wide used + limit bytes
551
+
552
+ # Skill / AI tooling installer
553
+ sentroy ai install [--claude] [--cursor] [--windsurf] [--agents] [--all] [--upgrade] [--check] [--source <path>] [--no-agents]
554
+ # Copies this SKILL.md into each tool's well-known skill directory.
555
+ # --upgrade : re-install only if the installed version differs from the bundled one
556
+ # (it is a version-aware refresh, NOT a force-overwrite).
557
+ # --check : report what would change without writing.
558
+ # --source : use a local SKILL.md path instead of the bundled copy.
559
+ ```
560
+
561
+ ## Versioning
562
+
563
+ - SDK + skill follow **semver**. Skill body is shipped inside `@sentroy-co/client-sdk`; bump the SDK to receive the latest skill copy.
564
+ - `sentroy ai install --upgrade` detects the version + sha markers in the footer below and reinstalls only when newer.
565
+ - Pin a major in production (`"@sentroy-co/client-sdk": "^2.0.0"`); minors add resources, patches fix bugs, majors may rename surfaces.
566
+
567
+ ## Where to look next
568
+
569
+ - `https://docs.sentroy.com/llms.txt` — full discovery index for LLMs
570
+ - `https://docs.sentroy.com/cli` — CLI reference
571
+ - `https://docs.sentroy.com/ai-skills` — this skill, its install paths, and the upgrade flow
572
+ - `https://docs.sentroy.com/mail` — mail product docs
573
+ - `https://docs.sentroy.com/storage` — storage product docs
574
+ - `https://docs.sentroy.com/auth-projects` — Auth-as-a-Service docs
575
+ - `https://raw.githubusercontent.com/Sentroy-Co/client-sdk/main/typescript/AGENTS.md` — the full 900-line TS reference (deep dive)
576
+
577
+ <!-- skill-version: 2.15.0 -->
@@ -100,6 +100,13 @@ async function loadSimpleWebAuthnBrowser(): Promise<{
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Browser localStorage-backed adapter. In React Native, `window.localStorage`
105
+ * is undefined, so this falls back to an in-memory adapter (session lost on
106
+ * app restart). For RN persistence, pass a custom adapter via
107
+ * `storage: createAsyncStorageAdapter(...)` or `createSecureStoreAdapter(...)`
108
+ * imported from `@sentroy-co/client-sdk/auth/react-native`.
109
+ */
103
110
  function localStorageAdapter(projectSlug: string): AuthStorageAdapter {
104
111
  if (typeof window === "undefined" || !window.localStorage) {
105
112
  return memoryStorageAdapter()
@@ -403,7 +410,7 @@ export class SentroyAuth {
403
410
  * çağırın. Başarılıysa user döner, fail'da null.
404
411
  */
405
412
  async consumeRedirectFragment(): Promise<SentroyAuthUser | null> {
406
- if (typeof window === "undefined") return null
413
+ if (typeof window === "undefined" || !window.location) return null
407
414
  const hash = window.location.hash.replace(/^#/, "")
408
415
  if (!hash) return null
409
416
  const params = new URLSearchParams(hash)
@@ -91,7 +91,7 @@ export function SentroyAuthProvider({
91
91
  // Social login redirect handler — fragment varsa otomatik consume
92
92
  useEffect(() => {
93
93
  if (!autoConsumeFragment) return
94
- if (typeof window === "undefined") return
94
+ if (typeof window === "undefined" || !window.location) return
95
95
  if (!window.location.hash.includes("access_token=")) return
96
96
  auth.consumeRedirectFragment().catch(() => {
97
97
  // Fragment varsa ama consume fail ise sessizce yut — caller