@alvera-ai/platform-sdk 0.10.0-rc.8 → 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.
- package/.agent/AGENTS.md +499 -0
- package/.agent/account_management.md +456 -0
- package/.agent/action_status_updaters.md +264 -0
- package/.agent/ai_agents.md +462 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +112 -0
- package/.agent/connected_apps.md +408 -0
- package/.agent/cookbook/_fixtures/README.md +106 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_customer.liquid +32 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/_customers_accounts_receivable_mdm.liquid +20 -0
- package/.agent/cookbook/_fixtures/accounts_receivable/stripe_customers_batch1.csv +5 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_generic_table.liquid +33 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_legal_entity.liquid +88 -0
- package/.agent/cookbook/_fixtures/foundation/_lead_submissions_foundation_mdm.liquid +48 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_appointment.liquid +47 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_mdm.liquid +24 -0
- package/.agent/cookbook/_fixtures/healthcare/_cahps_appointments_healthcare_patient.liquid +38 -0
- package/.agent/cookbook/_fixtures/healthcare/memorandum-of-association-01.png +0 -0
- package/.agent/cookbook/_fixtures/healthcare/sample_two_page.pdf +43 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_compliance_screening.liquid +59 -0
- package/.agent/cookbook/_fixtures/payment_risk/_compliance_screenings_payment_risk_mdm.liquid +36 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_mdm.liquid +30 -0
- package/.agent/cookbook/_fixtures/payment_risk/_payment_accounts_payment_risk_payment_account.liquid +55 -0
- package/.agent/cookbook/_setup/accounts_receivable.md +282 -0
- package/.agent/cookbook/_setup/foundation.md +277 -0
- package/.agent/cookbook/_setup/healthcare.md +279 -0
- package/.agent/cookbook/_setup/payment_risk.md +283 -0
- package/.agent/cookbook/action-status-updaters.md +212 -0
- package/.agent/cookbook/ai-agent-invoke.md +243 -0
- package/.agent/cookbook/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/bulk-ingest.md +254 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +622 -0
- package/.agent/cookbook/custom-tables.md +201 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/invite-team.md +194 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/rest-fetch.md +246 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +733 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +624 -0
- package/.agent/cookbook/system-templates.md +129 -0
- package/.agent/cookbook/triage-prospects-by-priority.md +533 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +559 -0
- package/.agent/data_sources.md +235 -0
- package/.agent/datalakes.md +714 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +190 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +417 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +126 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +547 -0
- package/.agent/type_naming.md +129 -0
- package/.agent/workflows.md +617 -0
- package/README.md +178 -0
- package/dist/bin/platform-sdk.d.mts +1 -0
- package/dist/bin/platform-sdk.mjs +106 -0
- package/dist/bin/platform-sdk.mjs.map +1 -0
- package/dist/index.d.mts +1310 -44116
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1194 -7344
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -10
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Capability: invite a teammate into your tenant"
|
|
3
|
+
summary: A capability walk for multi-user onboarding. An admin invites a colleague by email (`api.invitations.create`), the colleague signs up and is confirmed, signs in without a tenant to find the pending invite (`invitations.list`), accepts it (`invitations.accept`), and finally signs in tenant-scoped as a member. Shows the three session scopes — root, tenantless, tenant-scoped — in one flow.
|
|
4
|
+
industry: accounts_receivable
|
|
5
|
+
slug: invite-team
|
|
6
|
+
vitest_source:
|
|
7
|
+
- integration-tests/tests/accounts_receivable/invite-team.test.ts
|
|
8
|
+
- integration-tests/tests/accounts_receivable/bootstrap.test.ts
|
|
9
|
+
status: green
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Capability
|
|
13
|
+
|
|
14
|
+
**What you get:** a second person on your tenant, added entirely through the
|
|
15
|
+
SDK — no console, no out-of-band steps.
|
|
16
|
+
|
|
17
|
+
The flow touches all three session scopes (see `AGENTS.md` § SDK auth):
|
|
18
|
+
|
|
19
|
+
- **tenant-scoped** (the admin) — `api.invitations.create(tenantSlug, { email, role })`
|
|
20
|
+
- **root** — signs the new user up and confirms them
|
|
21
|
+
(`api.auth.signUp` + `api.admin.confirmUser`)
|
|
22
|
+
- **tenantless** (the invitee, before they belong anywhere) — lists pending
|
|
23
|
+
invites and accepts one (`api.invitations.list` / `api.invitations.accept`)
|
|
24
|
+
|
|
25
|
+
After accepting, the invitee re-authenticates **tenant-scoped** and is a member.
|
|
26
|
+
This capability is **global** — it works the same on every datalake's tenant.
|
|
27
|
+
|
|
28
|
+
# Walkthrough
|
|
29
|
+
|
|
30
|
+
The `_setup/accounts_receivable.md` bootstrap already provisioned the tenant and
|
|
31
|
+
left `api` as the admin's tenant-scoped client, with `tenantSlug` and
|
|
32
|
+
`ctx.sarahEmail` / `ctx.sarahPassword` populated. Root credentials come from the
|
|
33
|
+
`ALVERA_ROOT_EMAIL` / `ALVERA_ROOT_PASSWORD` env vars the validator already
|
|
34
|
+
exports.
|
|
35
|
+
|
|
36
|
+
## 001 — confirm the admin can invite
|
|
37
|
+
|
|
38
|
+
Before inviting, verify the current session is a tenant-scoped admin of the
|
|
39
|
+
expected tenant. `api.sessions.verify()` echoes the session's `tenant.slug` and
|
|
40
|
+
`role.name`. Only an admin can create invitations.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const { data: who } = await api.sessions.verify()
|
|
44
|
+
if (who.tenant?.slug !== tenantSlug) {
|
|
45
|
+
throw new Error(`expected an admin session on ${tenantSlug}, got ${who.tenant?.slug}`)
|
|
46
|
+
}
|
|
47
|
+
if (!/admin/i.test(who.role?.name ?? '')) {
|
|
48
|
+
throw new Error(`expected an admin role, got ${who.role?.name}`)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 002 — invite the teammate by email
|
|
53
|
+
|
|
54
|
+
The admin invites a colleague with their email and a role (`member`). The
|
|
55
|
+
invite is keyed on `(tenant, email)`, so re-inviting the same address returns a
|
|
56
|
+
422 with "already invited" — handle that as success so the step is idempotent on
|
|
57
|
+
re-runs. The teammate's per-run email is derived from `runSuffix` so parallel
|
|
58
|
+
runs don't collide.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
ctx.emmaEmail = `cookbook-emma-${runSuffix}@dev.local`
|
|
62
|
+
ctx.emmaPassword = 'CookbookPass1!'
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const { data: invite } = await api.invitations.create(tenantSlug, {
|
|
66
|
+
email: ctx.emmaEmail,
|
|
67
|
+
role: 'member',
|
|
68
|
+
})
|
|
69
|
+
if (invite.email !== ctx.emmaEmail || invite.role !== 'member') {
|
|
70
|
+
throw new Error(`unexpected invite: ${JSON.stringify(invite)}`)
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const detail = JSON.stringify(err)
|
|
74
|
+
if (!detail.includes('already invited')) throw err
|
|
75
|
+
// already invited on a re-run — fine
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 003 — root signs the teammate up and confirms them
|
|
80
|
+
|
|
81
|
+
The invitee needs an account before they can accept. Signup + confirmation are
|
|
82
|
+
root-scoped actions, so build a root client from the `ALVERA_ROOT_*` credentials
|
|
83
|
+
and use it. `confirmUser` activates the account so the invitee can sign in.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const rootSession = await createSession({
|
|
87
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
88
|
+
email: process.env.ALVERA_ROOT_EMAIL!,
|
|
89
|
+
password: process.env.ALVERA_ROOT_PASSWORD!,
|
|
90
|
+
})
|
|
91
|
+
const rootApi = createIsolatedPlatformApi({
|
|
92
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
93
|
+
sessionToken: rootSession.sessionToken,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const { data: emmaUser } = await rootApi.auth.signUp({
|
|
97
|
+
email: ctx.emmaEmail,
|
|
98
|
+
password: ctx.emmaPassword,
|
|
99
|
+
first_name: 'Emma',
|
|
100
|
+
last_name: 'Wilson',
|
|
101
|
+
})
|
|
102
|
+
await rootApi.admin.confirmUser(emmaUser.id!)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 004 — teammate signs in WITHOUT a tenant and finds the invite
|
|
106
|
+
|
|
107
|
+
The invitee has an account but belongs to no tenant yet, so they sign in
|
|
108
|
+
**tenantless** — `createSession` with no `tenantSlug`. The resulting session has
|
|
109
|
+
`tenant: null`. A tenantless client can list the invitations waiting for that
|
|
110
|
+
user. Find the one for this tenant by matching `tenant.slug`.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const emmaTenantless = await createSession({
|
|
114
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
115
|
+
email: ctx.emmaEmail,
|
|
116
|
+
password: ctx.emmaPassword,
|
|
117
|
+
// no tenantSlug → tenantless session
|
|
118
|
+
})
|
|
119
|
+
if (emmaTenantless.tenant !== null) {
|
|
120
|
+
throw new Error('expected a tenantless session (tenant should be null)')
|
|
121
|
+
}
|
|
122
|
+
const emmaTenantlessApi = createIsolatedPlatformApi({
|
|
123
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
124
|
+
sessionToken: emmaTenantless.sessionToken,
|
|
125
|
+
})
|
|
126
|
+
ctx.emmaTenantlessToken = emmaTenantless.sessionToken
|
|
127
|
+
|
|
128
|
+
const { data: invites } = await emmaTenantlessApi.invitations.list()
|
|
129
|
+
const ours = (invites.data ?? []).find((i) => i.tenant?.slug === tenantSlug)
|
|
130
|
+
if (!ours?.id) {
|
|
131
|
+
throw new Error(`no pending invite for ${tenantSlug} found for the new user`)
|
|
132
|
+
}
|
|
133
|
+
ctx.invitationId = ours.id
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 005 — teammate accepts the invitation
|
|
137
|
+
|
|
138
|
+
Accepting consumes the invite and creates the membership. The response echoes
|
|
139
|
+
the new `role` and the `tenant.slug` the user just joined.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const emmaTenantlessApi = createIsolatedPlatformApi({
|
|
143
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
144
|
+
sessionToken: ctx.emmaTenantlessToken,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const { data: membership } = await emmaTenantlessApi.invitations.accept(ctx.invitationId)
|
|
148
|
+
if (membership.tenant?.slug !== tenantSlug || membership.role !== 'member') {
|
|
149
|
+
throw new Error(`unexpected membership: ${JSON.stringify(membership)}`)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 006 — teammate signs in tenant-scoped as a member
|
|
154
|
+
|
|
155
|
+
Now that the membership exists, the invitee re-authenticates **with** the
|
|
156
|
+
tenant slug. The new session carries `tenant.slug` and a `member` role — they
|
|
157
|
+
are in.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const emmaScoped = await createSession({
|
|
161
|
+
baseUrl: process.env.ALVERA_BASE_URL!,
|
|
162
|
+
email: ctx.emmaEmail,
|
|
163
|
+
password: ctx.emmaPassword,
|
|
164
|
+
tenantSlug,
|
|
165
|
+
})
|
|
166
|
+
if (emmaScoped.tenant?.slug !== tenantSlug) {
|
|
167
|
+
throw new Error(`expected a tenant-scoped session on ${tenantSlug}`)
|
|
168
|
+
}
|
|
169
|
+
if (!/member/i.test(emmaScoped.role?.name ?? '')) {
|
|
170
|
+
throw new Error(`expected a member role, got ${emmaScoped.role?.name}`)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
# Gotchas
|
|
175
|
+
|
|
176
|
+
- **Invitations are created tenant-scoped, accepted tenantless.** The admin
|
|
177
|
+
calls `invitations.create(tenantSlug, …)` on their tenant-scoped client; the
|
|
178
|
+
invitee calls `invitations.list()` / `accept(id)` on a **tenantless** client
|
|
179
|
+
(they have no tenant yet). Mixing the scopes up is the #1 mistake here.
|
|
180
|
+
- **Re-inviting the same email 422s.** The `(tenant, email)` pair is unique —
|
|
181
|
+
catch "already invited" and treat it as success for idempotent re-runs.
|
|
182
|
+
- **The invitee re-authenticates twice.** Once tenantless (to find + accept the
|
|
183
|
+
invite), then again tenant-scoped (to act as a member). Accepting does not
|
|
184
|
+
upgrade the existing tenantless session in place — you mint a new one.
|
|
185
|
+
- **Signup + confirm are root-scoped.** A tenant admin can invite, but only root
|
|
186
|
+
can create and confirm the underlying user account.
|
|
187
|
+
|
|
188
|
+
# See also
|
|
189
|
+
|
|
190
|
+
- `AGENTS.md` § SDK auth + client construction — root / tenantless / tenant-scoped
|
|
191
|
+
- `account_management.md` — users, roles, invitations reference
|
|
192
|
+
- `_setup/accounts_receivable.md` — the bootstrap this walk starts from
|
|
193
|
+
- `integration-tests/tests/accounts_receivable/invite-team.test.ts` — the green
|
|
194
|
+
test these calls are lifted from
|