@alvera-ai/platform-sdk 0.10.0-rc.9 → 0.11.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 (66) hide show
  1. package/.agent/AGENTS.md +499 -0
  2. package/.agent/account_management.md +456 -0
  3. package/.agent/action_status_updaters.md +264 -0
  4. package/.agent/ai_agents.md +462 -0
  5. package/.agent/ai_sandbox.md +265 -0
  6. package/.agent/async.md +112 -0
  7. package/.agent/connected_apps.md +408 -0
  8. package/.agent/cookbook/_fixtures/README.md +106 -0
  9. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
  10. package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
  11. package/.agent/cookbook/_fixtures/accounts_receivable/stripe_customers_batch1.csv +5 -0
  12. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
  13. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
  14. package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
  15. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
  16. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
  17. package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
  18. package/.agent/cookbook/_fixtures/healthcare/memorandum-of-association-01.png +0 -0
  19. package/.agent/cookbook/_fixtures/healthcare/sample_two_page.pdf +43 -0
  20. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
  21. package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
  22. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
  23. package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
  24. package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
  25. package/.agent/cookbook/_setup/foundation.md +277 -0
  26. package/.agent/cookbook/_setup/healthcare.md +279 -0
  27. package/.agent/cookbook/_setup/payment_risk.md +283 -0
  28. package/.agent/cookbook/action-status-updaters.md +212 -0
  29. package/.agent/cookbook/ai-agent-invoke.md +243 -0
  30. package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
  31. package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
  32. package/.agent/cookbook/bulk-ingest.md +254 -0
  33. package/.agent/cookbook/contact-us-triage-with-llm.md +622 -0
  34. package/.agent/cookbook/custom-tables.md +201 -0
  35. package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
  36. package/.agent/cookbook/invite-team.md +194 -0
  37. package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
  38. package/.agent/cookbook/rest-fetch.md +246 -0
  39. package/.agent/cookbook/sanctions-screening-with-agent-review.md +733 -0
  40. package/.agent/cookbook/score-leads-with-llm-categorization.md +624 -0
  41. package/.agent/cookbook/system-templates.md +129 -0
  42. package/.agent/cookbook/triage-prospects-by-priority.md +533 -0
  43. package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
  44. package/.agent/data_activation_clients.md +559 -0
  45. package/.agent/data_sources.md +235 -0
  46. package/.agent/datalakes.md +714 -0
  47. package/.agent/debugging.md +137 -0
  48. package/.agent/errors.md +190 -0
  49. package/.agent/generic_tables.md +351 -0
  50. package/.agent/interoperability_contracts.md +417 -0
  51. package/.agent/mdm.md +293 -0
  52. package/.agent/mutations.md +126 -0
  53. package/.agent/templates.md +98 -0
  54. package/.agent/tool-call-configs.md +90 -0
  55. package/.agent/tools.md +547 -0
  56. package/.agent/type_naming.md +129 -0
  57. package/.agent/workflows.md +617 -0
  58. package/README.md +178 -0
  59. package/dist/bin/platform-sdk.d.mts +1 -0
  60. package/dist/bin/platform-sdk.mjs +106 -0
  61. package/dist/bin/platform-sdk.mjs.map +1 -0
  62. package/dist/index.d.mts +1310 -44116
  63. package/dist/index.d.mts.map +1 -1
  64. package/dist/index.mjs +1194 -7344
  65. package/dist/index.mjs.map +1 -1
  66. package/package.json +18 -9
@@ -0,0 +1,456 @@
1
+ # Account management
2
+
3
+ Every SDK call resolves against a **trust boundary** with three
4
+ coordinates:
5
+
6
+ ```
7
+ who User (human) OR ApiKey / PAT (agent)
8
+ where Tenant (+ optional Datalake)
9
+ what Role assumed by this session
10
+ ```
11
+
12
+ Account management is how those three coordinates are established
13
+ and reasoned about. This page covers six SDK namespaces that work
14
+ together:
15
+
16
+ | Namespace | Methods |
17
+ |--------------------|-----------------------------------------------|
18
+ | `api.auth` | `signUp` |
19
+ | `api.admin` | `confirmUser` — **dev fixture only** |
20
+ | `api.sessions` | `verify` |
21
+ | `api.tenants` | `create`, `list` |
22
+ | `api.invitations` | `create`, `list`, `accept` |
23
+ | `api.ping` | unauthenticated health check (`GET /api/ping`) |
24
+ | `createSession` | mint a Bearer (top-level helper, not a namespace) |
25
+ | `revokeSession` | revoke the current Bearer (top-level helper, parallel to `createSession`) |
26
+
27
+ Two top-level concerns sit above the surface:
28
+
29
+ 1. **Who is calling** — a human (Bearer) or an agent (API key / PAT)
30
+ 2. **What that caller can do** — gated by role + by credential type
31
+
32
+ Both branches differ in lifecycle, capability, and whether they
33
+ can be created programmatically. See §3 "Access control".
34
+
35
+ ## 1. Identity — humans
36
+
37
+ Humans authenticate by exchanging email+password for a Bearer.
38
+ The Bearer carries the trust-boundary coordinates and travels in
39
+ the `Authorization: Bearer <token>` header on every subsequent
40
+ request.
41
+
42
+ ### Three Bearer scopes
43
+
44
+ `createSession` mints a Bearer at one of three scopes:
45
+
46
+ ```typescript
47
+ import { createSession } from '@alvera-ai/platform-sdk'
48
+
49
+ // root admin (no tenant) — for confirmUser, signUp on behalf of, etc.
50
+ const root = await createSession({
51
+ baseUrl, email: rootEmail, password: rootPassword,
52
+ })
53
+
54
+ // tenantless — authenticated user who has not picked a tenant yet
55
+ const tenantless = await createSession({
56
+ baseUrl, email, password,
57
+ })
58
+ // tenantless.tenant === null
59
+
60
+ // tenant-scoped — the canonical Bearer for tenant operations
61
+ const tenant = await createSession({
62
+ baseUrl, email, password, tenantSlug,
63
+ })
64
+ // tenant.tenant.slug === tenantSlug
65
+ // tenant.role.name === 'tenant_admin' | 'member' | ...
66
+ ```
67
+
68
+ See `AGENTS.md` "Sessions come in three scopes" for when each is
69
+ appropriate. Tenantless is a transient state used once: to create
70
+ a tenant (creator path) or accept an invitation (invitee path).
71
+
72
+ ### Explicit teardown — `revokeSession`
73
+
74
+ The inverse of `createSession`. Revokes the current Bearer on
75
+ the server and short-circuits the local `api` instance — every
76
+ subsequent request through the same instance returns 401:
77
+
78
+ ```typescript
79
+ import { revokeSession } from '@alvera-ai/platform-sdk'
80
+
81
+ await revokeSession() // DELETE /api/v1/sessions
82
+ // api.* calls now return 401
83
+ ```
84
+
85
+ Use this for graceful sign-out flows in long-running consumers
86
+ (CLIs, daemons). Letting the Bearer reach `expires_at` is fine
87
+ for short-lived scripts, but explicit revocation removes the
88
+ token server-side immediately and reduces the window of misuse
89
+ if the token leaks.
90
+
91
+ ### Liveness check — `api.ping`
92
+
93
+ A no-auth health probe used by load balancers, smoke tests, and
94
+ SDK consumer setup checks. The response confirms the platform
95
+ is reachable and reports the build version + database
96
+ connectivity:
97
+
98
+ ```typescript
99
+ const { data } = await api.ping()
100
+ // data.status === 'ok'
101
+ // data.version — platform build version
102
+ // data.database_status === 'connected' | 'disconnected'
103
+ // data.timestamp — ISO 8601
104
+ ```
105
+
106
+ `api.ping` works on any api instance regardless of session
107
+ state — useful as the first call in tests to detect a stale
108
+ local server before authentication failures muddy the diagnostic.
109
+
110
+ ### Sign up
111
+
112
+ ```typescript
113
+ const { data: user } = await rootApi.auth.signUp({
114
+ email: 'sarah@example.com',
115
+ password: '...',
116
+ first_name: 'Sarah',
117
+ last_name: 'Mitchell',
118
+ })
119
+ // user.id is a UUID; user.confirmed_at is null
120
+ ```
121
+
122
+ A newly signed-up user CANNOT sign in until confirmed. The
123
+ production confirmation path is **email-driven**: the platform
124
+ sends a confirmation email; the user clicks a link; the platform
125
+ records `confirmed_at`. **This email flow is not exposed in the
126
+ SDK** — it's a browser+email round-trip outside the API surface.
127
+
128
+ ### `api.admin.confirmUser` is a dev fixture, not a production API
129
+
130
+ ```typescript
131
+ // DEV / VITEST ONLY — do NOT call from production consumer code
132
+ await rootApi.admin.confirmUser(userId)
133
+ ```
134
+
135
+ `api.admin.confirmUser` bypasses the email-confirmation gate. It
136
+ exists so integration tests can simulate a confirmed user without
137
+ a real inbox round-trip. Production consumers MUST NOT use it:
138
+
139
+ - It requires a root Bearer (impossible to obtain in customer
140
+ environments).
141
+ - The email gate exists to prove a real person controls the
142
+ inbox — programmatically bypassing it defeats the human-gate
143
+ invariant (see §3).
144
+ - Future platform releases may restrict it further or remove it
145
+ from the public surface entirely.
146
+
147
+ The method ships in the typed client to support integration-test
148
+ fixtures; treat it as a test-only helper, not a production API.
149
+
150
+ ### `api.sessions.verify` — "who am I, where, with what role"
151
+
152
+ ```typescript
153
+ const { data: ctx } = await api.sessions.verify()
154
+ // ctx.tenant?.slug — current tenant (null if tenantless)
155
+ // ctx.role?.name — role assumed in this session
156
+ // ctx.user — { id, first_name, last_name, ... }
157
+ ```
158
+
159
+ Use cases:
160
+
161
+ 1. **Pre-flight check** before performing role-gated operations —
162
+ confirm the Bearer is tenant-scoped + has the expected role.
163
+ 2. **Bearer validity probe** — if the call throws, the Bearer is
164
+ expired or revoked; re-mint via `createSession`.
165
+ 3. **Cached-Bearer rehydration** — when persisting Bearers across
166
+ processes (integration tests, CLI sessions), verify before
167
+ reusing.
168
+
169
+ For pattern (3), call `verify()` on the stored Bearer and only
170
+ re-mint via `createSession` if the verify call fails.
171
+
172
+ ## 2. Tenant
173
+
174
+ A **tenant** is the top-level data sovereignty boundary. Tenants
175
+ own datalakes; datalakes own everything else. A user can belong
176
+ to many tenants; a Bearer is scoped to exactly one.
177
+
178
+ ### Two paths to a tenant-scoped Bearer
179
+
180
+ ```
181
+ ┌─ creator path ─┐ ┌─ invitee path ─┐
182
+ │ │
183
+ api.auth.signUp api.auth.signUp
184
+ │ │
185
+ api.admin.confirmUser* api.admin.confirmUser* (* production: email link)
186
+ │ │
187
+ createSession (no tenantSlug) createSession (no tenantSlug)
188
+ │ │
189
+ api.tenants.create api.invitations.list → .accept
190
+ │ │
191
+ createSession ({ tenantSlug }) createSession ({ tenantSlug })
192
+ ▼ ▼
193
+ ┌──── tenant-scoped Bearer ────┐
194
+ ```
195
+
196
+ Both paths converge on a tenant-scoped Bearer. The creator
197
+ becomes `tenant_admin` of the new tenant; the invitee assumes the
198
+ role specified in their invitation.
199
+
200
+ ### `api.tenants.create`
201
+
202
+ ```typescript
203
+ const { data } = await tenantlessApi.tenants.create({
204
+ name: 'Acme Health',
205
+ })
206
+ // data.tenant.id, data.tenant.slug — server-derived
207
+ // data.session_token — see "Gotchas" below
208
+ ```
209
+
210
+ The slug is server-derived from `name` per the universal slug
211
+ rules (see `type_naming.md` "Never pre-compute the slug
212
+ client-side"). Use the returned slug for downstream calls.
213
+
214
+ **Gotcha**: the response carries a `session_token` field, but this
215
+ is an *incidental* token — do not treat it as a freshly minted
216
+ tenant-scoped Bearer. The authoritative pattern is to call
217
+ `createSession({ ..., tenantSlug })` after `tenants.create`. This
218
+ gives a fresh Bearer with all role + tenant claims correctly
219
+ populated.
220
+
221
+ ### `api.tenants.list`
222
+
223
+ ```typescript
224
+ const { data: tenants } = await api.tenants.list()
225
+ // tenants.data: TenantResponse[] — every tenant the current Bearer can see
226
+ ```
227
+
228
+ A root Bearer sees every tenant; a tenant-scoped Bearer typically
229
+ sees only its own. Use this for tenant pickers in apps where a
230
+ user belongs to multiple tenants.
231
+
232
+ ## 3. Access control
233
+
234
+ The platform separates **who you are** from **what you can do**.
235
+ Identity (§1) proves the first; this section covers the second.
236
+
237
+ ### Roles — two vocabularies, one consumer
238
+
239
+ The platform exposes two distinct role vocabularies on the wire,
240
+ each rooted in a different schema. Knowing which surface
241
+ demands which vocabulary is the difference between a clean
242
+ invite and a 422.
243
+
244
+ #### Vocabulary A: Invitation enum (3 values)
245
+
246
+ What you SUBMIT when creating an invitation. The
247
+ `InvitationRequest.role` field is a closed 3-value enum:
248
+
249
+ | Invitation `role` | Result on accept |
250
+ |-------------------|----------------------------------------|
251
+ | `'member'` | Standard tenant user |
252
+ | `'researcher'` | Tokenized-only access; blocked from regulated schema |
253
+ | `'admin'` | Full tenant management |
254
+
255
+ ```typescript
256
+ const { data: invite } = await api.invitations.create(tenantSlug, {
257
+ email: 'emma@example.com',
258
+ role: 'admin', // NOT 'tenant_admin' — that's the role-name vocab
259
+ })
260
+ ```
261
+
262
+ Passing any value outside this 3-set returns a 422 on `/role`.
263
+
264
+ #### Vocabulary B: Role-record names (what `session.role.name` carries)
265
+
266
+ What you READ from a tenant-scoped Bearer's role record. The
267
+ session carries a role-record name string that's distinct from
268
+ the invitation enum:
269
+
270
+ | `session.role.name` | Scope | Source vocab |
271
+ |-------------------------|------------------------------------|--------------|
272
+ | `'tenant_admin'` | Full tenant management | invitation `'admin'` |
273
+ | `'member'` | Standard tenant user | invitation `'member'` |
274
+ | `'tenant_api'` | Programmatic tenant access (most API keys) | provisioned, not invited |
275
+ | `'datalake_admin'` | Full datalake management (requires `datalake_id` on session) | console-mediated |
276
+ | `'researcher'` | Tokenized-only datalake access | invitation `'researcher'` |
277
+ | `'datalake_api'` | Programmatic datalake access | provisioned, not invited |
278
+
279
+ ```typescript
280
+ const session = await createSession({ baseUrl, email, password, tenantSlug })
281
+ // session.role.name === 'tenant_admin' if accepted from a role: 'admin' invitation
282
+ // session.role.name === 'member' if accepted from a role: 'member' invitation
283
+ // session.role.name === 'researcher' if accepted from a role: 'researcher' invitation
284
+ ```
285
+
286
+ The asymmetry is deliberate but easy to trip over: the
287
+ invitation enum value `'admin'` becomes the role-record name
288
+ `'tenant_admin'`. The `'member'` and `'researcher'` values
289
+ carry through unchanged.
290
+
291
+ #### Why two vocabularies
292
+
293
+ The Invitation/Membership schema (Vocabulary A) is the
294
+ coarse-grained role assignment surfaced at invite time. The
295
+ Role-record system (Vocabulary B) is the fine-grained policy
296
+ entity that drives row-level access and permission checks at every
297
+ request. They co-exist because the invitation captures the
298
+ caller's intent ("admin-tier user"), while the role record
299
+ captures the platform's enforcement vocabulary
300
+ ("tenant-scoped admin role").
301
+
302
+ Datalake-scoped role records (`datalake_admin`, `researcher`
303
+ when used at datalake scope, `datalake_api`) require the
304
+ session to carry a `datalake_id` in addition to a `tenant_id`.
305
+ Most SDK consumers stay at tenant scope; datalake-scoped
306
+ sessions appear in console-mediated flows and are uncommon in
307
+ SDK-consumer code.
308
+
309
+ Reserved roles (`platform_admin`, `system`, `system_api`,
310
+ `bot_*`) bypass tenant-based access isolation and are
311
+ provisioned only by the platform itself — they never appear on
312
+ consumer-minted sessions or invitations and are out of scope
313
+ for SDK consumers.
314
+
315
+ ### Inviting humans into a tenant
316
+
317
+ ```typescript
318
+ // As tenant_admin
319
+ const { data: invite } = await api.invitations.create(tenantSlug, {
320
+ email: 'emma@example.com',
321
+ role: 'member',
322
+ })
323
+
324
+ // As the invitee (tenantless Bearer)
325
+ const { data: invites } = await tenantlessApi.invitations.list()
326
+ const mine = invites.data.find(i => i.tenant.slug === tenantSlug)
327
+ const { data: membership } = await tenantlessApi.invitations.accept(mine.id)
328
+
329
+ // Then mint a tenant-scoped Bearer
330
+ const session = await createSession({ baseUrl, email, password, tenantSlug })
331
+ // session.role.name === 'member'
332
+ ```
333
+
334
+ The duplicate-invite envelope carries `detail` containing
335
+ "already invited" — see `errors.md` for the canonical 422 shape.
336
+ Treat it as a benign idempotency signal on retry.
337
+
338
+ ### Agent authentication — two-tier model
339
+
340
+ Agents (programmatic callers) authenticate via two distinct
341
+ credential types with very different capability profiles:
342
+
343
+ | Credential type | Capability | Created programmatically? |
344
+ |-----------------|---------------------------|------------------------------|
345
+ | **API key** | Write-only, very limited | **Yes** — safe by scope |
346
+ | **PAT** (delegated) | Full per delegating human's role | **No** — human-gated |
347
+
348
+ The rule: **anything that gives an agent broad access is gated by
349
+ a human**. Specifically:
350
+
351
+ - **API keys** can be minted programmatically because their
352
+ capability is intrinsically narrow (write-only, scoped to
353
+ specific endpoints). A leaked API key can deposit data; it
354
+ cannot exfiltrate it.
355
+ - **PATs** delegate a human's full role to an agent. Because the
356
+ resulting agent-Bearer has the same capability as the human,
357
+ PAT issuance MUST require human consent at mint time — the
358
+ human is the gate.
359
+ - **User signup + confirmation** is human-gated (email flow) for
360
+ the same reason: a programmatically-created confirmed user
361
+ could mint Bearers without any human in the loop. The
362
+ email-link confirmation step proves a real person controls the
363
+ inbox.
364
+
365
+ ### What the SDK exposes for agent auth
366
+
367
+ **Today the SDK exposes neither credential-lifecycle surface.**
368
+ API key and PAT minting / listing / revocation happen via
369
+ operator-mediated consoles, not via SDK methods. The absence
370
+ of these lifecycle methods from the SDK is the contract.
371
+
372
+ When the SDK does need to *authenticate as* an agent (uncommon in
373
+ fixture code; common in production agent runtimes), the credential
374
+ travels in the appropriate request header — see the credentials
375
+ your operator provides for the exact header convention. A session
376
+ Bearer minted via `createSession` is also accepted for any
377
+ SDK-driven agent workflow.
378
+
379
+ ## 4. Lifecycle
380
+
381
+ Bearers and credentials have distinct lifecycles. Summary:
382
+
383
+ | Credential | Expires? | Revocation |
384
+ |------------|---------------------------------------|-------------------|
385
+ | Bearer (session) | Yes — `expires_in` (default 24h, max 30d) | Server-side revoke |
386
+ | API key | No — until explicit revoke | Operator console |
387
+ | PAT | Configurable at issue time | Operator console |
388
+
389
+ Bearer expiry surfaces as a 401 on the next call after expiry —
390
+ re-mint via `createSession`. The SDK does NOT auto-refresh
391
+ Bearers. For long-running agent processes, mint short-lived
392
+ Bearers per logical unit of work rather than holding one for
393
+ hours.
394
+
395
+ ## 5. Error envelopes
396
+
397
+ All endpoints in this page emit the standard JSON:API envelope
398
+ documented in `errors.md`. Common rejections:
399
+
400
+ | `source.pointer` / shape | Cause |
401
+ |-------------------------------------------------|----------------------------------------------|
402
+ | `/email` "has already been taken" | `auth.signUp` with an existing email |
403
+ | `/password` "is too short" | password below minimum length |
404
+ | top-level 401 (no envelope) | Bearer missing, expired, or revoked |
405
+ | top-level 403 (no envelope) | role lacks permission for the action |
406
+ | `/email` "already invited" | duplicate `invitations.create` |
407
+ | `/tenant_slug` "not found" | tenant slug doesn't exist or invisible to Bearer |
408
+
409
+ The `tenants.create` 422 is typically `/name` uniqueness — names
410
+ must be unique platform-wide. Re-attempt with a different name or
411
+ treat as "already exists" via `api.tenants.list` lookup.
412
+
413
+ ## 6. Gotchas
414
+
415
+ 1. **`api.admin.confirmUser` is dev-only.** Production user
416
+ confirmation happens via emailed link, outside the SDK. Treat
417
+ the SDK method as a test fixture; do not ship code that calls
418
+ it from a customer-facing path.
419
+
420
+ 2. **The `session_token` returned by `tenants.create` is
421
+ incidental.** Always re-mint a fresh tenant-scoped Bearer via
422
+ `createSession({ ..., tenantSlug })` after creating a tenant.
423
+ This guarantees clean role+tenant claims on the Bearer.
424
+
425
+ 3. **Tenantless Bearer is a transient state.** Only two
426
+ operations are useful with it: `tenants.create` (creator path)
427
+ and `invitations.list`/`.accept` (invitee path). For everything
428
+ else, mint a tenant-scoped Bearer first.
429
+
430
+ 4. **`api.sessions.verify` is the source of truth for current
431
+ scope.** Don't infer tenant or role from the Bearer string or
432
+ from the `tenants.create` response — call `verify` if you
433
+ need to know.
434
+
435
+ 5. **No SDK methods for API key or PAT lifecycle.** Credential
436
+ issuance is operator-console-mediated. If your agent process
437
+ needs an API key, the operator provisions it out-of-band; the
438
+ SDK consumes it but does not create it.
439
+
440
+ 6. **Bearers do not auto-refresh.** A long-running process holding
441
+ a Bearer for >24h will see 401s after expiry. Either mint
442
+ short-lived Bearers per logical unit of work, or wrap calls in
443
+ a re-mint-on-401 retry helper local to your code (the SDK does
444
+ not provide one).
445
+
446
+ 7. **Invitation acceptance is idempotent at the membership level
447
+ but not at the invitation level.** A second `accept(id)` call
448
+ on the same invitation will 422 (already accepted), but the
449
+ first acceptance's membership is unaffected. Treat retries via
450
+ the "already accepted" 422 the same as "already invited" — a
451
+ benign idempotency signal.
452
+
453
+ 8. **Multi-tenant Bearers don't exist.** A Bearer is scoped to
454
+ exactly one tenant at mint time. To act as multiple tenants
455
+ concurrently, mint one Bearer per tenant and use
456
+ `createIsolatedPlatformApi` (see `AGENTS.md`).