@sentroy-co/client-sdk 2.13.8 → 2.14.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 (59) hide show
  1. package/README.md +24 -5
  2. package/dist/auth/admin/index.d.ts +111 -10
  3. package/dist/auth/admin/index.d.ts.map +1 -1
  4. package/dist/auth/admin/index.js +125 -20
  5. package/dist/auth/admin/index.js.map +1 -1
  6. package/dist/auth/client.d.ts +127 -1
  7. package/dist/auth/client.d.ts.map +1 -1
  8. package/dist/auth/client.js +361 -3
  9. package/dist/auth/client.js.map +1 -1
  10. package/dist/auth/index.d.ts +1 -1
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js.map +1 -1
  13. package/dist/auth/react/index.d.ts +63 -4
  14. package/dist/auth/react/index.d.ts.map +1 -1
  15. package/dist/auth/react/index.js +180 -1
  16. package/dist/auth/react/index.js.map +1 -1
  17. package/dist/auth/types.d.ts +55 -0
  18. package/dist/auth/types.d.ts.map +1 -1
  19. package/dist/cli/ai.d.ts +35 -0
  20. package/dist/cli/ai.d.ts.map +1 -0
  21. package/dist/cli/ai.js +399 -0
  22. package/dist/cli/ai.js.map +1 -0
  23. package/dist/cli/args.d.ts +62 -0
  24. package/dist/cli/args.d.ts.map +1 -0
  25. package/dist/cli/args.js +199 -0
  26. package/dist/cli/args.js.map +1 -0
  27. package/dist/cli/env.d.ts.map +1 -1
  28. package/dist/cli/env.js +8 -2
  29. package/dist/cli/env.js.map +1 -1
  30. package/dist/cli/format.d.ts +37 -0
  31. package/dist/cli/format.d.ts.map +1 -0
  32. package/dist/cli/format.js +129 -0
  33. package/dist/cli/format.js.map +1 -0
  34. package/dist/cli/index.d.ts +8 -2
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +128 -25
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/cli/mail.d.ts +25 -0
  39. package/dist/cli/mail.d.ts.map +1 -0
  40. package/dist/cli/mail.js +253 -0
  41. package/dist/cli/mail.js.map +1 -0
  42. package/dist/cli/storage.d.ts +28 -0
  43. package/dist/cli/storage.d.ts.map +1 -0
  44. package/dist/cli/storage.js +189 -0
  45. package/dist/cli/storage.js.map +1 -0
  46. package/package.json +8 -2
  47. package/skill/SKILL.md +542 -0
  48. package/src/auth/admin/index.ts +227 -31
  49. package/src/auth/client.ts +438 -4
  50. package/src/auth/index.ts +9 -0
  51. package/src/auth/react/index.tsx +255 -4
  52. package/src/auth/types.ts +66 -0
  53. package/src/cli/ai.ts +440 -0
  54. package/src/cli/args.ts +225 -0
  55. package/src/cli/env.ts +10 -2
  56. package/src/cli/format.ts +147 -0
  57. package/src/cli/index.ts +147 -25
  58. package/src/cli/mail.ts +363 -0
  59. package/src/cli/storage.ts +307 -0
package/skill/SKILL.md ADDED
@@ -0,0 +1,542 @@
1
+ ---
2
+ name: sentroy
3
+ version: 2.14.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
+ ### Python
69
+
70
+ > **Note:** Python/PHP/Go SDK packages are in development; today they map 1:1 to raw HTTP calls — see the cURL recipes.
71
+
72
+ ```bash
73
+ pip install sentroy
74
+ ```
75
+
76
+ ```python
77
+ from sentroy import Sentroy
78
+
79
+ sentroy = Sentroy(
80
+ base_url="https://sentroy.com",
81
+ company_slug="acme",
82
+ access_token=os.environ["SENTROY_API_KEY"],
83
+ )
84
+
85
+ print(sentroy.domains.list())
86
+ ```
87
+
88
+ ### PHP
89
+
90
+ ```bash
91
+ composer require sentroy/client-sdk
92
+ ```
93
+
94
+ ```php
95
+ use Sentroy\Sentroy;
96
+
97
+ $sentroy = new Sentroy([
98
+ 'baseUrl' => 'https://sentroy.com',
99
+ 'companySlug' => 'acme',
100
+ 'accessToken' => getenv('SENTROY_API_KEY'),
101
+ ]);
102
+
103
+ print_r($sentroy->domains->list());
104
+ ```
105
+
106
+ ### Go
107
+
108
+ ```bash
109
+ go get github.com/Sentroy-Co/client-sdk/go
110
+ ```
111
+
112
+ ```go
113
+ import "github.com/Sentroy-Co/client-sdk/go/sentroy"
114
+
115
+ client := sentroy.New(sentroy.Config{
116
+ BaseURL: "https://sentroy.com",
117
+ CompanySlug: "acme",
118
+ AccessToken: os.Getenv("SENTROY_API_KEY"),
119
+ })
120
+
121
+ domains, err := client.Domains.List(ctx)
122
+ ```
123
+
124
+ ### cURL
125
+
126
+ ```bash
127
+ curl -H "Authorization: Bearer $SENTROY_API_KEY" \
128
+ https://sentroy.com/api/companies/acme/domains
129
+ ```
130
+
131
+ ## Common task recipes
132
+
133
+ ### 1. Send a templated email
134
+
135
+ ```ts
136
+ const result = await sentroy.send.email({
137
+ domainId: "dom_abc", // REQUIRED — the verified sending domain
138
+ templateId: "tpl_welcome",
139
+ to: "alice@example.com",
140
+ from: "noreply@acme.com", // must belong to the verified domain above
141
+ variables: { firstName: "Alice", confirmUrl: "https://acme.com/c/abc" },
142
+ });
143
+ // → { jobId: "job_…", mailLogId: "log_…", status: "queued", scheduledAt?: "2026-…" }
144
+ ```
145
+
146
+ Common error: `400 "from address domain not verified"` — verify the domain first (recipe 3).
147
+
148
+ ### 2. Send a raw email (no template)
149
+
150
+ ```ts
151
+ await sentroy.send.email({
152
+ domainId: "dom_abc", // REQUIRED
153
+ to: "bob@example.com",
154
+ from: "alerts@acme.com",
155
+ subject: "Build #182 failed",
156
+ html: "<p>See log…</p>",
157
+ text: "See log…",
158
+ });
159
+ // → { jobId: "job_…", mailLogId: "log_…", status: "queued" }
160
+ ```
161
+
162
+ Common error: `403 send.execute` — the token lacks the `send.execute` permission.
163
+
164
+ ### 3. Create + verify a domain (raw HTTP)
165
+
166
+ The TS SDK currently only exposes `sentroy.domains.list()` and `sentroy.domains.get(id)`. Create + verify must go through raw HTTP today.
167
+
168
+ ```bash
169
+ # Create the domain — returns the DNS records you need to publish
170
+ curl -X POST \
171
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
172
+ -H "Content-Type: application/json" \
173
+ -d '{"name":"acme.com"}' \
174
+ https://sentroy.com/api/companies/acme/domains
175
+ # → { id: "dom_…", status: "pending", dnsRecords: [
176
+ # { type: "TXT", name: "@", value: "v=spf1 …" },
177
+ # { type: "CNAME", name: "s1._domainkey", value: "s1.dkim.…" },
178
+ # { type: "CNAME", name: "s2._domainkey", value: "s2.dkim.…" },
179
+ # { type: "TXT", name: "_dmarc", value: "v=DMARC1 …" },
180
+ # ]}
181
+
182
+ # After publishing DNS, trigger re-check:
183
+ curl -X POST \
184
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
185
+ https://sentroy.com/api/companies/acme/domains/dom_abc/verify
186
+ # → { status: "verified" | "pending" | "failed", checks: { spf, dkim, dmarc } }
187
+ ```
188
+
189
+ The DNS records returned cover SPF, two DKIM selectors, and DMARC — publish all four for full deliverability.
190
+
191
+ Common error: `status: "pending"` for up to 60 min as DNS propagates. Poll, don't loop tightly.
192
+
193
+ ### 4. Create a mailbox (raw HTTP)
194
+
195
+ The TS SDK currently only exposes `sentroy.mailboxes.list()`. Create goes through raw HTTP.
196
+
197
+ ```bash
198
+ curl -X POST \
199
+ -H "Authorization: Bearer $SENTROY_API_KEY" \
200
+ -H "Content-Type: application/json" \
201
+ -d '{
202
+ "domainId": "dom_abc",
203
+ "localPart": "support",
204
+ "displayName": "Acme Support",
205
+ "password": "'"$(uuidgen)"'"
206
+ }' \
207
+ https://sentroy.com/api/companies/acme/mailboxes
208
+ # → { id: "mb_…", address: "support@acme.com" }
209
+ ```
210
+
211
+ Common error: `409 "mailbox already exists"` — local part collision on the domain.
212
+
213
+ ### 5. Upload a file to a bucket (single, public)
214
+
215
+ `bucketSlug` is the **first positional argument**; the field is `body` (Blob / Buffer / stream), and visibility is `isPublic: boolean`.
216
+
217
+ ```ts
218
+ const file = await fs.promises.readFile("./hero.jpg");
219
+ const media = await sentroy.media.upload("marketing", {
220
+ body: file,
221
+ filename: "hero.jpg",
222
+ contentType: "image/jpeg",
223
+ isPublic: true, // → served from cdn.sentroy.com/f/<id>
224
+ });
225
+ // → { id: "med_…", url: "https://cdn.sentroy.com/f/med_…", size: 482103 }
226
+ ```
227
+
228
+ Common error: `413 "file too big"` — single-shot upload limit applies; for very large files, see the note below.
229
+
230
+ > **Large files (>100MB):** upload via the Storage dashboard, which uses a 3-parallel multipart pool internally. Programmatic multipart from the SDK is roadmap.
231
+
232
+ ### 6. List media in a bucket (paginated)
233
+
234
+ `bucketSlug` is positional. Pagination is **offset-based** (`skip`/`limit`), not cursor-based.
235
+
236
+ ```ts
237
+ const page = await sentroy.media.list("marketing", {
238
+ limit: 50,
239
+ skip: 0, // offset; bump by `limit` for next page
240
+ type: "image", // image | video | audio | doc | other
241
+ folder: "/heroes",
242
+ q: "hero", // optional search
243
+ sort: "createdAt", // optional
244
+ dir: "desc", // optional
245
+ });
246
+ // → { items: [...], total: 137, limit: 50, skip: 0, sort?: "createdAt", dir?: "desc" }
247
+ ```
248
+
249
+ ### 7. Sign up an end-user via Auth Project (cURL)
250
+
251
+ ```bash
252
+ curl -X POST \
253
+ -H "Authorization: Bearer $SENTROY_APS_KEY" \
254
+ -H "Content-Type: application/json" \
255
+ -d '{"email":"alice@example.com","password":"hunter2","name":"Alice"}' \
256
+ https://auth.sentroy.com/api/v1/auth/my-app/signup
257
+ # → { user: {...}, accessToken: "<jwt>", refreshToken: "..." }
258
+ ```
259
+
260
+ 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`.
261
+
262
+ ## Resource API surface
263
+
264
+ All paths are relative to `https://sentroy.com/api/companies/<slug>` unless noted.
265
+
266
+ ### Mail — domains
267
+
268
+ | Method | Path | Description | Permission |
269
+ |---|---|---|---|
270
+ | GET | `/domains` | List domains | `domains.view` |
271
+ | POST | `/domains` | Create + return DNS records | `domains.create` |
272
+ | GET | `/domains/{id}` | Detail + verification state | `domains.view` |
273
+ | POST | `/domains/{id}/verify` | Re-check DNS | `domains.edit` |
274
+ | DELETE | `/domains/{id}` | Remove | `domains.delete` |
275
+
276
+ ### Mail — mailboxes
277
+
278
+ | Method | Path | Description | Permission |
279
+ |---|---|---|---|
280
+ | GET | `/mailboxes` | List | `mailboxes.manage` |
281
+ | POST | `/mailboxes` | Create | `mailboxes.manage` |
282
+ | PATCH | `/mailboxes/{id}` | Rename / change password / quota | `mailboxes.manage` |
283
+ | DELETE | `/mailboxes/{id}` | Remove | `mailboxes.manage` |
284
+
285
+ ### Mail — templates
286
+
287
+ | Method | Path | Description | Permission |
288
+ |---|---|---|---|
289
+ | GET | `/templates` | List | `templates.manage` |
290
+ | POST | `/templates` | Create (name/subject/body accept LocalizedString) | `templates.manage` |
291
+ | GET | `/templates/{id}` | Detail | `templates.manage` |
292
+ | PATCH | `/templates/{id}` | Update | `templates.manage` |
293
+ | DELETE | `/templates/{id}` | Remove | `templates.manage` |
294
+
295
+ ### Mail — inbox
296
+
297
+ 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.
298
+
299
+ | Method | Path | Description | Permission |
300
+ |---|---|---|---|
301
+ | GET | `/inbox?mailbox=<addr>&folder=<inbox\|sent\|trash>&unread=<bool>` | List messages | `inbox.view` |
302
+ | GET | `/inbox/{uid}?mailbox=<addr>&folder=<folder>` | Message + parsed parts | `inbox.view` |
303
+ | POST | `/inbox/{uid}/read?mailbox=<addr>&folder=<folder>` | Mark read | `inbox.view` |
304
+ | DELETE | `/inbox/{uid}?mailbox=<addr>&folder=<folder>` | Trash | `inbox.view` |
305
+
306
+ ### Mail — send / suppressions / webhooks / logs / analytics
307
+
308
+ | Method | Path | Description | Permission |
309
+ |---|---|---|---|
310
+ | POST | `/send` | Send (template or raw)¹ | `send.execute` |
311
+ | GET | `/suppressions` | Bounced/complained addresses | `suppressions.manage` |
312
+ | POST | `/suppressions` | Add manually | `suppressions.manage` |
313
+ | DELETE | `/suppressions/{addr}` | Remove | `suppressions.manage` |
314
+ | GET | `/webhooks` | List endpoints | `webhooks.manage` |
315
+ | POST | `/webhooks` | Subscribe (`events: ["delivered","bounced",...]`) | `webhooks.manage` |
316
+ | GET | `/logs` | Send log (filter `status`, `domain`, `from`, `to`) | `logs.view` |
317
+ | GET | `/logs/{id}` | Message timeline | `logs.view` |
318
+ | GET | `/analytics` | Aggregate counts (param `days=7|30|90`) | `logs.view` |
319
+
320
+ ¹ TS SDK method `sentroy.send.email()` calls `POST /send` for ergonomics — pass either `templateId` + `variables` or `subject` + `html`/`text`, plus the required `domainId`.
321
+
322
+ ### Storage — buckets
323
+
324
+ | Method | Path | Description | Permission |
325
+ |---|---|---|---|
326
+ | GET | `/buckets` | List | `storage.view` |
327
+ | POST | `/buckets` | Create | `buckets.create` |
328
+ | GET | `/buckets/{slug}` | Detail | `storage.view` |
329
+ | PATCH | `/buckets/{slug}` | Rename / visibility | `buckets.edit` |
330
+ | DELETE | `/buckets/{slug}` | Force-delete cascade | `buckets.delete` |
331
+
332
+ ### Storage — media
333
+
334
+ 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.
335
+
336
+ | Method | Path | Description | Permission |
337
+ |---|---|---|---|
338
+ | GET | `/buckets` | List buckets | `storage.view` |
339
+ | GET | `/buckets/{slug}` | Bucket detail | `storage.view` |
340
+ | GET | `/buckets/{slug}/media` | List (`limit`, `skip`, `type`, `folder`, `q`, `sort`, `dir`) | `storage.view` |
341
+ | GET | `/buckets/{slug}/media/{mediaId}` | Detail | `storage.view` |
342
+ | GET | `/buckets/{slug}/media/{mediaId}/download` | Authenticated download URL | `storage.view` |
343
+ | GET | `/usage` | Per-bucket usage stats | `storage.view` |
344
+ | GET | `/storage-quota` | Used + limit bytes (company-wide) | `storage.view` |
345
+
346
+ ### Env vault
347
+
348
+ 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.
349
+
350
+ | Method | Path | Description |
351
+ |---|---|---|
352
+ | POST | `/api/env-vault/push` | Full sync up (CLI flag `--delete-missing` controls removal of vault keys absent locally) |
353
+ | POST | `/api/env-vault/fetch` | Full snapshot down (server-authoritative state) |
354
+ | GET | `/api/env-vault/public` | Browser-safe subset only (keys flagged public) |
355
+
356
+ ### Auth-as-a-Service (uses `aps_` key, host = `auth.sentroy.com`)
357
+
358
+ > **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.
359
+
360
+ | Method | Path | Description |
361
+ |---|---|---|
362
+ | POST | `/api/v1/auth/{slug}/signup` | Create end-user |
363
+ | POST | `/api/v1/auth/{slug}/login` | Issue JWT |
364
+ | POST | `/api/v1/auth/{slug}/refresh` | Refresh JWT |
365
+ | POST | `/api/v1/auth/{slug}/logout` | Revoke session |
366
+ | GET | `/api/v1/auth/{slug}/userinfo` | Bearer-validated user |
367
+ | POST | `/api/v1/auth/{slug}/verify-email` | Confirm token |
368
+ | POST | `/api/v1/auth/{slug}/password-reset/request` | Send reset mail |
369
+ | POST | `/api/v1/auth/{slug}/password-reset/confirm` | Apply new password |
370
+ | GET | `/api/v1/auth/{slug}/jwks.json` | Public keys for JWT verify |
371
+
372
+ ## LocalizedString gotcha
373
+
374
+ 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.
375
+
376
+ ```ts
377
+ // Plain string — applied to all locales
378
+ await sentroy.templates.create({
379
+ name: "Welcome",
380
+ subject: "Welcome to Acme",
381
+ body: "<p>Hi {{firstName}}</p>",
382
+ });
383
+
384
+ // Localized — different copy per locale
385
+ await sentroy.templates.create({
386
+ name: { tr: "Hoş geldin", en: "Welcome" },
387
+ subject: { tr: "Acme'ye hoş geldin", en: "Welcome to Acme" },
388
+ body: { tr: "<p>Merhaba {{firstName}}</p>", en: "<p>Hi {{firstName}}</p>" },
389
+ });
390
+ ```
391
+
392
+ If a recipient's locale is missing the SDK falls back to `en` then to the first available key.
393
+
394
+ ## Permission scopes
395
+
396
+ Request the **minimum** scope. Wildcards exist but should be a last resort.
397
+
398
+ ```
399
+ domains.view domains.create domains.edit domains.delete domains.manage
400
+ mailboxes.manage
401
+ templates.manage
402
+ inbox.view
403
+ audience.manage
404
+ send.execute
405
+ logs.view
406
+ webhooks.manage
407
+ suppressions.manage
408
+ api-keys.manage
409
+ smtp.manage
410
+ members.manage
411
+ storage.view
412
+ buckets.create buckets.edit buckets.delete
413
+ media.upload media.delete media.reorder
414
+ ```
415
+
416
+ - **Wildcards:** `<resource>.manage` grants every action on that resource. Use only when the integration genuinely needs full CRUD.
417
+ - **Scoped (legacy):** `domains.domain:<id>` — all actions on one specific domain.
418
+ - **Scoped (granular):** `domains.domain:<id>:<action>` (`view|edit|delete|create`) — one action on one domain.
419
+ - **Mailbox-scoped inbox:** `inbox.mailbox:<email>` — read only one mailbox's mail.
420
+
421
+ **Dashboard-only scopes (not relevant to `stk_` REST callers):**
422
+ `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.
423
+
424
+ Owner / admin company members bypass scope checks; `member` role is granular.
425
+
426
+ ## Errors
427
+
428
+ Standard JSON envelope on failure — note there is **no** `success`, `code`, or `details` field:
429
+
430
+ ```json
431
+ { "data": null, "error": "Human-readable message" }
432
+ ```
433
+
434
+ On success the envelope is `{ "data": <payload>, "error": null }`. Branch on the HTTP status code and read `error` for the user-displayable string.
435
+
436
+ | Status | Meaning | Typical fix |
437
+ |---|---|---|
438
+ | 400 | Validation failed | Read `error`; fix payload |
439
+ | 401 | Missing / malformed / expired token | Re-issue `stk_` or `aps_` |
440
+ | 403 | Token lacks the required permission | Add scope in dashboard |
441
+ | 404 | Resource not in this company (or wrong slug) | Check `companySlug` and resource id |
442
+ | 409 | Conflict (duplicate slug, mailbox, etc.) | Use a different identifier |
443
+ | 413 | Payload too large | Compress or upload via dashboard |
444
+ | 422 | Semantic error (e.g. `from` domain unverified) | Resolve precondition |
445
+ | 429 | Rate-limited | Honor `Retry-After` header; back off |
446
+ | 500 / 502 / 503 | Server error | Retry with exponential backoff |
447
+
448
+ ## Rate limits
449
+
450
+ 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.
451
+
452
+ ## Gotchas & footguns
453
+
454
+ 1. **`stk_` plaintext is shown only on create.** It is irretrievable afterward — store it the moment you create one.
455
+ 2. **`tokenPrefix` (first 12 chars)** is the only identifier visible in lists / dashboard after creation. Use it to disambiguate tokens, not the full secret.
456
+ 3. **`baseUrl` = platform root**, never a subdomain. The SDK rewrites `/api/mail/*` → `mail.sentroy.com` and `/api/storage/*` → `storage.sentroy.com` for you.
457
+ 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.
458
+ 5. **Avatar / logo uploads** use the `DirectAvatarUpload` React helper, **not** `MediaManagerTrigger` — no bucket picker, just crop + POST.
459
+ 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.
460
+ 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.
461
+ 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.
462
+ 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.
463
+ 10. **Storage quota:** preflight large uploads with `GET /storage-quota` — `413` after upload start wastes bytes against your budget.
464
+ 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.
465
+
466
+ ## CLI
467
+
468
+ The TS package ships a `sentroy` binary. Install once globally or use via `npx`:
469
+
470
+ ```bash
471
+ npm install -g @sentroy-co/client-sdk # global
472
+ npx sentroy <command> # ad-hoc
473
+ ```
474
+
475
+ **Auth via env (preferred):**
476
+
477
+ ```bash
478
+ export SENTROY_API_KEY=stk_…
479
+ export SENTROY_COMPANY_SLUG=acme
480
+ # Env vault subgroup uses its own scoped token:
481
+ export SENTROY_ENV_API_KEY=stk_env_…
482
+ ```
483
+
484
+ Or per-invocation flags: `--token`, `--company-slug`, `--url` (defaults to `https://sentroy.com`).
485
+
486
+ **Global flags:** `--token`, `--url`, `--company-slug`, `--output=json|table` (default `table`). Every list/get command supports `--output=json` for scripting / piping into `jq`.
487
+
488
+ **Commands:**
489
+
490
+ ```bash
491
+ # Env vault sync
492
+ sentroy env push # local .env → vault
493
+ sentroy env pull # vault → local .env
494
+ sentroy env list # show all keys
495
+ sentroy env diff # local vs. vault
496
+
497
+ # Mail
498
+ sentroy mail templates list
499
+ sentroy mail templates get <id>
500
+ sentroy mail domains list
501
+ sentroy mail mailboxes list
502
+ sentroy mail inbox list [--mailbox=<addr>] [--folder=inbox|sent|trash] [--unread]
503
+ sentroy mail suppressions list
504
+ sentroy mail logs list [--status=delivered|bounced|deferred] [--domain=<name>] [--from=<iso>] [--to=<iso>]
505
+ sentroy mail logs get <id>
506
+ sentroy mail webhooks list
507
+ sentroy mail analytics [--days=7|30|90]
508
+
509
+ # Storage
510
+ sentroy storage buckets list
511
+ sentroy storage buckets get <bucketSlug>
512
+ sentroy storage media list <bucketSlug> [--type=image|video|audio|doc|other] [--folder=<path>] [--q=<query>]
513
+ sentroy storage media get <bucketSlug> <mediaId>
514
+ sentroy storage usage
515
+ sentroy storage quota # company-wide used + limit bytes
516
+
517
+ # Skill / AI tooling installer
518
+ sentroy ai install [--claude] [--cursor] [--windsurf] [--agents] [--all] [--upgrade] [--check] [--source <path>] [--no-agents]
519
+ # Copies this SKILL.md into each tool's well-known skill directory.
520
+ # --upgrade : re-install only if the installed version differs from the bundled one
521
+ # (it is a version-aware refresh, NOT a force-overwrite).
522
+ # --check : report what would change without writing.
523
+ # --source : use a local SKILL.md path instead of the bundled copy.
524
+ ```
525
+
526
+ ## Versioning
527
+
528
+ - SDK + skill follow **semver**. Skill body is shipped inside `@sentroy-co/client-sdk`; bump the SDK to receive the latest skill copy.
529
+ - `sentroy ai install --upgrade` detects the version + sha markers in the footer below and reinstalls only when newer.
530
+ - Pin a major in production (`"@sentroy-co/client-sdk": "^2.0.0"`); minors add resources, patches fix bugs, majors may rename surfaces.
531
+
532
+ ## Where to look next
533
+
534
+ - `https://docs.sentroy.com/llms.txt` — full discovery index for LLMs
535
+ - `https://docs.sentroy.com/cli` — CLI reference
536
+ - `https://docs.sentroy.com/ai-skills` — this skill, its install paths, and the upgrade flow
537
+ - `https://docs.sentroy.com/mail` — mail product docs
538
+ - `https://docs.sentroy.com/storage` — storage product docs
539
+ - `https://docs.sentroy.com/auth-projects` — Auth-as-a-Service docs
540
+ - `https://raw.githubusercontent.com/Sentroy-Co/client-sdk/main/typescript/AGENTS.md` — the full 900-line TS reference (deep dive)
541
+
542
+ <!-- skill-version: 2.14.0 -->