@alvera-ai/platform-sdk 0.10.0-rc.2 → 0.10.0-rc.21
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 +440 -0
- package/.agent/account_management.md +455 -0
- package/.agent/action_status_updaters.md +262 -0
- package/.agent/ai_agents.md +423 -0
- package/.agent/ai_sandbox.md +265 -0
- package/.agent/async.md +111 -0
- package/.agent/connected_apps.md +407 -0
- package/.agent/cookbook/_fixtures/README.md +99 -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/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/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/appointment-review-sms-workflow.md +761 -0
- package/.agent/cookbook/birthday-greeting-sms-trigger.md +656 -0
- package/.agent/cookbook/contact-us-triage-with-llm.md +603 -0
- package/.agent/cookbook/dunning-sms-for-delinquent.md +619 -0
- package/.agent/cookbook/kyc-notification-on-account-activation.md +619 -0
- package/.agent/cookbook/sanctions-screening-with-agent-review.md +711 -0
- package/.agent/cookbook/score-leads-with-llm-categorization.md +602 -0
- package/.agent/cookbook/welcome-sms-for-customers.md +607 -0
- package/.agent/data_activation_clients.md +557 -0
- package/.agent/data_sources.md +234 -0
- package/.agent/datalakes.md +712 -0
- package/.agent/debugging.md +137 -0
- package/.agent/errors.md +196 -0
- package/.agent/generic_tables.md +351 -0
- package/.agent/interoperability_contracts.md +351 -0
- package/.agent/mdm.md +293 -0
- package/.agent/mutations.md +152 -0
- package/.agent/templates.md +98 -0
- package/.agent/tool-call-configs.md +90 -0
- package/.agent/tools.md +546 -0
- package/.agent/type_naming.md +131 -0
- package/.agent/workflows.md +601 -0
- package/README.md +46 -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 +1200 -43201
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1859 -7319
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -10
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# AI sandbox — the safety contract
|
|
2
|
+
|
|
3
|
+
Alvera accepts AI-authored fragments from external tools (Claude
|
|
4
|
+
Code, Cursor, ChatGPT, in-tenant AI agents, direct API clients)
|
|
5
|
+
and runs them against tenant data. **None of those fragments
|
|
6
|
+
ever reach arbitrary code execution.** Every author surface — a
|
|
7
|
+
Liquid template body, a SQL fragment, an agent prompt — passes
|
|
8
|
+
through a three-layer sandbox that bounds blast radius.
|
|
9
|
+
|
|
10
|
+
This page names the three layers, the contract each one
|
|
11
|
+
enforces, and where each one's wire-level details live in the
|
|
12
|
+
rest of the corpus. It's the consumer-side landing for "why is
|
|
13
|
+
it safe to put my AI-authored body here".
|
|
14
|
+
|
|
15
|
+
The three layers are independent — any one alone would bound
|
|
16
|
+
the worst case — and they compose into defense-in-depth.
|
|
17
|
+
|
|
18
|
+
## Layer 1 — Compliance gates (who sees what)
|
|
19
|
+
|
|
20
|
+
Two consumer-set fields decide whether a request reaches raw
|
|
21
|
+
PHI/PII/PCI or only the tokenized projection. Both must
|
|
22
|
+
authorize regulated reach for raw data to flow:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
session role AI agent data_access → outcome
|
|
26
|
+
────────────── ─────────────────── ───────
|
|
27
|
+
member or admin regulated regulated reachable
|
|
28
|
+
member or admin unregulated tokenized only
|
|
29
|
+
member or admin (no agent on path) regulated reachable
|
|
30
|
+
researcher any value tokenized only
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Session role** is pinned on the membership at invitation
|
|
34
|
+
time (vocabulary A — `'member' | 'researcher' | 'admin'`; the
|
|
35
|
+
resulting session carries the corresponding role-record name
|
|
36
|
+
— see `account_management.md` §3).
|
|
37
|
+
- **AI agent `data_access`** is `'regulated' | 'unregulated'`,
|
|
38
|
+
pinned at agent creation (immutable post-create — see
|
|
39
|
+
`ai_agents.md`). Default is `'unregulated'`; `'regulated'` is
|
|
40
|
+
opt-in.
|
|
41
|
+
|
|
42
|
+
A `researcher` session can never reach raw PHI regardless of how
|
|
43
|
+
the agent is configured. An `'unregulated'` agent can never
|
|
44
|
+
reach raw PHI regardless of session role. Tenant isolation
|
|
45
|
+
(every row carries a tenant id; queries filter on the session's
|
|
46
|
+
tenant) AND-composes with both of these so a request only sees
|
|
47
|
+
the slice that *all three* allow.
|
|
48
|
+
|
|
49
|
+
The composition rule is fail-safe: a workflow spec that omits
|
|
50
|
+
`data_access` is treated as `'unregulated'`. Author-side
|
|
51
|
+
mistakes default to less reach, never more.
|
|
52
|
+
|
|
53
|
+
## Layer 2 — Liquid sandbox (what can execute)
|
|
54
|
+
|
|
55
|
+
Every consumer-authored template body — interop contract
|
|
56
|
+
`template_config.body`, AI agent `prompt_config.body`, workflow
|
|
57
|
+
filter / decision / action bodies, action `trigger_template`,
|
|
58
|
+
data activation client `row_filter`, tool call `body` templates
|
|
59
|
+
— is a Liquid template. Not arbitrary code. Liquid is the trust
|
|
60
|
+
boundary between consumer fragments and the platform runtime.
|
|
61
|
+
|
|
62
|
+
### What Liquid bodies CANNOT do
|
|
63
|
+
|
|
64
|
+
- Invoke runtime code. There is no `{% eval %}` or equivalent.
|
|
65
|
+
- Read the filesystem, open sockets, or reach the network.
|
|
66
|
+
- Mutate state outside the render's own local variable scope.
|
|
67
|
+
- Call any platform capability beyond the explicit custom-filter
|
|
68
|
+
allowlist (below).
|
|
69
|
+
|
|
70
|
+
Liquid's stock tag set (`{% if %}`, `{% for %}`, `{% assign %}`,
|
|
71
|
+
`{% unless %}`, `{% capture %}`, etc.) is preserved as-is.
|
|
72
|
+
`{% capture %}` writes to a Liquid-scope variable; it is not a
|
|
73
|
+
runtime eval escape hatch.
|
|
74
|
+
|
|
75
|
+
### The 11-filter custom allowlist
|
|
76
|
+
|
|
77
|
+
Beyond stock Liquid, the platform exposes exactly **eleven**
|
|
78
|
+
custom filters. Adding new ones requires a platform release —
|
|
79
|
+
consumers cannot extend the set:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
e164 Phone string → E.164 format for SMS.
|
|
83
|
+
Example: {{ patient.phone | e164 }} → +15555550201
|
|
84
|
+
|
|
85
|
+
age Years from a birth date.
|
|
86
|
+
Example: {{ patient.birth_date | age }} → 66
|
|
87
|
+
|
|
88
|
+
date Format a datetime, optional add/subtract.
|
|
89
|
+
Example: {{ start | date: '%Y-%m-%d', 'subtract', '24 hours' }}
|
|
90
|
+
→ 2026-03-14
|
|
91
|
+
|
|
92
|
+
to_json Encode a map / array to a JSON string.
|
|
93
|
+
Example: {{ event_dataset | to_json }} → {"name":"…"}
|
|
94
|
+
|
|
95
|
+
json_escape Escape a value for safe interpolation inside a
|
|
96
|
+
JSON string literal (quotes, backslashes,
|
|
97
|
+
control chars).
|
|
98
|
+
Example: "name": "{{ p.name | json_escape }}"
|
|
99
|
+
→ "name": "O\"Brien"
|
|
100
|
+
|
|
101
|
+
now Current UTC datetime, ISO 8601.
|
|
102
|
+
Example: {{ '' | now }} → 2026-03-15T14:30:00Z
|
|
103
|
+
|
|
104
|
+
uuid Generate a new v4 UUID.
|
|
105
|
+
Example: {{ '' | uuid }} → a1b2c3d4-…
|
|
106
|
+
|
|
107
|
+
parse_date Parse a date string with a format spec.
|
|
108
|
+
Example: {{ '31/12/2023' | parse_date: '{D}/{0M}/{YY}' }}
|
|
109
|
+
→ 2023-12-31
|
|
110
|
+
|
|
111
|
+
convert_time Convert a time between formats.
|
|
112
|
+
Example: {{ '10:00 pm' | convert_time: '{h12}:{m} {am}' }}
|
|
113
|
+
→ 22:00:00
|
|
114
|
+
|
|
115
|
+
random Random integer in a range.
|
|
116
|
+
Example: {{ 10 | random }} → 0..10
|
|
117
|
+
|
|
118
|
+
tz_offset Current UTC offset for an IANA timezone string.
|
|
119
|
+
Example: {{ timezone | tz_offset }} → -04:00
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A common trap: `| json` (Shopify / Jekyll spelling) is NOT in
|
|
123
|
+
the allowlist. The lookalike falls through to a generic
|
|
124
|
+
stringification that produces unparseable output. Always use
|
|
125
|
+
`| to_json`. See `workflows.md` gotcha 9 for the diagnostic
|
|
126
|
+
trail this trap leaves.
|
|
127
|
+
|
|
128
|
+
### Write-time and render-time validation
|
|
129
|
+
|
|
130
|
+
- **Parse on write.** Every body is parsed before storage.
|
|
131
|
+
Syntactically invalid Liquid is rejected with a 422 on the
|
|
132
|
+
body field — bad templates never reach a render site.
|
|
133
|
+
- **Schema on render.** When the embed declares an
|
|
134
|
+
`output_schema` (most often a JSON Schema describing the
|
|
135
|
+
expected shape of the rendered JSON), the rendered output is
|
|
136
|
+
validated against the schema before reaching the downstream
|
|
137
|
+
consumer. Shape drift surfaces at the point of production, not
|
|
138
|
+
at the point of use.
|
|
139
|
+
|
|
140
|
+
## Layer 3 — SQL fragment boundary (what queries can do)
|
|
141
|
+
|
|
142
|
+
SQL is the second consumer-author surface: WHERE clauses on
|
|
143
|
+
dataset searches (`api.datasets.createUserSearch.search_query`,
|
|
144
|
+
see `data_activation_clients.md` §6.5). Liquid renders some of
|
|
145
|
+
these from templates; others arrive directly as raw SQL
|
|
146
|
+
fragments. Either path lands at the same boundary.
|
|
147
|
+
|
|
148
|
+
### What SQL fragments CAN do
|
|
149
|
+
|
|
150
|
+
- SELECT against the regulated tables exposed by the dataset
|
|
151
|
+
search aliases (`rp` for `regulated_patients`, `rc` for
|
|
152
|
+
`regulated_customers`, etc. — see `data_activation_clients.md`
|
|
153
|
+
§6.5 SQL aliases).
|
|
154
|
+
- Filter results into the platform's `search_results`
|
|
155
|
+
accumulator scoped to the materialised search id. This is the
|
|
156
|
+
one write the query path performs, and the platform builds it
|
|
157
|
+
on the consumer's behalf — the consumer authors the WHERE
|
|
158
|
+
clause, not the INSERT.
|
|
159
|
+
|
|
160
|
+
### What SQL fragments CANNOT do
|
|
161
|
+
|
|
162
|
+
- Read PHI/PII/PCI if Layer 1 disallows it. The dedicated
|
|
163
|
+
query-connection pool runs with reader-tier credentials
|
|
164
|
+
scoped to the session's tenant; cross-tenant reach is
|
|
165
|
+
physically blocked at the connection.
|
|
166
|
+
- INSERT, UPDATE, or DELETE against any table other than the
|
|
167
|
+
materialised search-results accumulator the platform manages.
|
|
168
|
+
- Invoke functions or extensions outside the read-only set
|
|
169
|
+
Postgres grants the pool. A consumer-authored fragment that
|
|
170
|
+
tries to call a write-grant function gets a permission denied
|
|
171
|
+
error from the database, not from the application.
|
|
172
|
+
|
|
173
|
+
### Parameterisation
|
|
174
|
+
|
|
175
|
+
Values that get interpolated into the SQL (typically via Liquid
|
|
176
|
+
filters that render a value into the WHERE clause) are bound as
|
|
177
|
+
parameters, not concatenated into the SQL string. SQL injection
|
|
178
|
+
via consumer-authored fragments is structurally impossible: the
|
|
179
|
+
fragment is the WHERE *expression*, with values supplied via
|
|
180
|
+
positional bindings the platform builds.
|
|
181
|
+
|
|
182
|
+
## How the three layers compose
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
┌─────────────────────────────────────────────┐
|
|
186
|
+
│ Layer 1 — compliance gates │
|
|
187
|
+
│ session role AND agent data_access │
|
|
188
|
+
│ AND tenant id (RLS) │
|
|
189
|
+
│ Decides: which schema (regulated vs │
|
|
190
|
+
│ unregulated) and which tenant the query │
|
|
191
|
+
│ hits. │
|
|
192
|
+
└────────────────────┬────────────────────────┘
|
|
193
|
+
│
|
|
194
|
+
┌────────────────────▼────────────────────────┐
|
|
195
|
+
│ Layer 2 — Liquid sandbox │
|
|
196
|
+
│ 11-filter allowlist │
|
|
197
|
+
│ + parse-on-write │
|
|
198
|
+
│ + render-time output_schema check │
|
|
199
|
+
│ Decides: what the consumer-authored body │
|
|
200
|
+
│ can express. No VM reach, no FS/network, │
|
|
201
|
+
│ no eval escape hatch. │
|
|
202
|
+
└────────────────────┬────────────────────────┘
|
|
203
|
+
│
|
|
204
|
+
┌────────────────────▼────────────────────────┐
|
|
205
|
+
│ Layer 3 — SQL fragment boundary │
|
|
206
|
+
│ parameterised bindings │
|
|
207
|
+
│ + reader-tier connection pool │
|
|
208
|
+
│ + INSERT-only on search_results │
|
|
209
|
+
│ Decides: what SQL fragments the WHERE │
|
|
210
|
+
│ clause path can execute. Read-most, │
|
|
211
|
+
│ write-one. │
|
|
212
|
+
└─────────────────────────────────────────────┘
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Each layer is independently sufficient: bypassing any single
|
|
216
|
+
layer still leaves the other two enforcing the contract. The
|
|
217
|
+
compound rule is "all three must permit" — read PHI requires
|
|
218
|
+
authorising session role AND regulated-mode agent (when an
|
|
219
|
+
agent is on the path) AND tenant id match.
|
|
220
|
+
|
|
221
|
+
## Auditability
|
|
222
|
+
|
|
223
|
+
Every LLM call the platform makes on behalf of a consumer is
|
|
224
|
+
recorded with cloud-storage pointers to the exact input payload
|
|
225
|
+
the agent received and the exact response the provider
|
|
226
|
+
returned. The audit record outlives the host run that produced
|
|
227
|
+
it — a compliance auditor can later replay any agent's input
|
|
228
|
+
and verify what data class (regulated vs tokenized) actually
|
|
229
|
+
reached the model. The audit surface is admin-facing today; the
|
|
230
|
+
guarantees it captures back-stop Layer 1.
|
|
231
|
+
|
|
232
|
+
## Consumer takeaways
|
|
233
|
+
|
|
234
|
+
1. **Default to `'unregulated'` agent `data_access`.** Opt into
|
|
235
|
+
`'regulated'` only when the action genuinely needs raw
|
|
236
|
+
values, and pair with a session role that's authorised.
|
|
237
|
+
|
|
238
|
+
2. **Use only the 11 custom filters** named in Layer 2. Bodies
|
|
239
|
+
that reference other filter names will silently fall through
|
|
240
|
+
to stock Liquid behavior and produce malformed output.
|
|
241
|
+
|
|
242
|
+
3. **Anchor JSON bodies on `to_json`.** When a template body
|
|
243
|
+
must produce valid JSON (most contexts of MDM input,
|
|
244
|
+
workflow context_mapping, action metadata), wrap nested
|
|
245
|
+
values with `| to_json` and never with `| json`.
|
|
246
|
+
|
|
247
|
+
4. **Treat SQL WHERE fragments as expressions, not statements.**
|
|
248
|
+
The dataset search surface owns the SELECT and the INSERT
|
|
249
|
+
into `search_results`. Consumer authoring is constrained to
|
|
250
|
+
the WHERE clause; everything else is platform-built.
|
|
251
|
+
|
|
252
|
+
5. **Pin output shapes with `output_schema`.** Where the embed
|
|
253
|
+
supports it (most TemplateConfig surfaces), populating
|
|
254
|
+
`output_schema` turns render-time shape drift into a clean
|
|
255
|
+
422 instead of a downstream JSON-decode error two queues
|
|
256
|
+
later.
|
|
257
|
+
|
|
258
|
+
## Related
|
|
259
|
+
|
|
260
|
+
- `ai_agents.md` §2 — `data_access` mode (Layer 1)
|
|
261
|
+
- `account_management.md` §3 — session role vocabularies (Layer 1)
|
|
262
|
+
- `interoperability_contracts.md` §2 — template_config body shape (Layer 2)
|
|
263
|
+
- `workflows.md` gotcha 9 — the `| to_json` vs `| json` trap (Layer 2)
|
|
264
|
+
- `data_activation_clients.md` §6.5 — dataset search SQL aliases (Layer 3)
|
|
265
|
+
- `mdm.md` — verification surface that composes with Layer 1
|
package/.agent/async.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Async and readiness
|
|
2
|
+
|
|
3
|
+
Some operations are async on the server. The create response
|
|
4
|
+
returns immediately with the resource in a non-terminal status;
|
|
5
|
+
downstream operations must wait for the resource to reach its
|
|
6
|
+
terminal status before proceeding.
|
|
7
|
+
|
|
8
|
+
## Canonical wait shape
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
async function waitUntilReady<T extends { status: string }>(
|
|
12
|
+
fetcher: () => Promise<{ data: T }>,
|
|
13
|
+
isTerminal: (status: string) => 'ready' | 'failed' | 'pending',
|
|
14
|
+
{ intervalMs = 1000, timeoutMs = 60_000 } = {},
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
const deadline = Date.now() + timeoutMs
|
|
17
|
+
while (true) {
|
|
18
|
+
const { data } = await fetcher()
|
|
19
|
+
const state = isTerminal(data.status)
|
|
20
|
+
if (state === 'ready') return data
|
|
21
|
+
if (state === 'failed') {
|
|
22
|
+
throw new Error(`failed: ${JSON.stringify(data)}`)
|
|
23
|
+
}
|
|
24
|
+
if (Date.now() > deadline) {
|
|
25
|
+
throw new Error(`timed out waiting for ready`)
|
|
26
|
+
}
|
|
27
|
+
await new Promise((r) => setTimeout(r, intervalMs))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The shape is the same across resources; only the terminal-
|
|
33
|
+
success / terminal-failure status values differ.
|
|
34
|
+
|
|
35
|
+
## Per-resource readiness signals
|
|
36
|
+
|
|
37
|
+
The terminal-success and terminal-failure status values are
|
|
38
|
+
resource-specific. Common examples:
|
|
39
|
+
|
|
40
|
+
| Resource | Terminal success | Terminal failure |
|
|
41
|
+
|-----------------------------------|------------------|-------------------------|
|
|
42
|
+
| Datalake (create + migrate) | `ready` | — (no terminal failure) |
|
|
43
|
+
| Data Activation Client run | `success` | `error` |
|
|
44
|
+
| AWS Lambda tool deploy | `deployed` | `failed` |
|
|
45
|
+
| Workflow run | `completed` | `failed` |
|
|
46
|
+
|
|
47
|
+
The Datalake row is the odd one: its top-level `status` enum is
|
|
48
|
+
`new | processing | ready` only — there is no terminal failure
|
|
49
|
+
value. A migration that fails leaves the datalake at
|
|
50
|
+
`processing` (or `new` if it never started). The failure
|
|
51
|
+
surfaces in the admin console's Migration Logs view, which is
|
|
52
|
+
not exposed over the SDK. Consumers polling for datalake
|
|
53
|
+
readiness must therefore rely on `timeoutMs` to bound the wait;
|
|
54
|
+
the `'failed'` branch of `waitUntilReady` never fires for this
|
|
55
|
+
resource.
|
|
56
|
+
|
|
57
|
+
The AWS Lambda tool row keys on `tool.body.stack_status` (the
|
|
58
|
+
CloudFormation deploy status nested under the polymorphic
|
|
59
|
+
`body`), not on the top-level `tool.status` enum (which is
|
|
60
|
+
`draft | active | inactive | error | marked_for_deletion`).
|
|
61
|
+
`tool.status` flips to `'active'` after a successful deploy
|
|
62
|
+
but `stack_status` is the more precise readiness signal — see
|
|
63
|
+
`tools.md` §2 for the per-body field set.
|
|
64
|
+
|
|
65
|
+
Each resource's MD §5 (Lifecycle) names its terminal-success
|
|
66
|
+
and terminal-failure status enums. If your resource isn't in
|
|
67
|
+
the table above, its MD is authoritative.
|
|
68
|
+
|
|
69
|
+
## Downstream resources MUST wait
|
|
70
|
+
|
|
71
|
+
Downstream operations (creating child resources, running
|
|
72
|
+
workflows, invoking tools) MUST NOT proceed until the parent
|
|
73
|
+
resource is in a terminal-success status. Posting a child
|
|
74
|
+
resource against an unready parent typically surfaces as a
|
|
75
|
+
500 (server-internal) or a 422 with a "parent not ready"
|
|
76
|
+
detail — both are recoverable but indicate a missing wait in
|
|
77
|
+
the caller.
|
|
78
|
+
|
|
79
|
+
## Idempotency on create
|
|
80
|
+
|
|
81
|
+
POST creates are NOT idempotent by default. Repeating a POST
|
|
82
|
+
with the same body yields a 422 on uniqueness fields (typically
|
|
83
|
+
the name within scope). Consumers handling retry should:
|
|
84
|
+
|
|
85
|
+
1. Catch the post-HTTP throw (see `errors.md`).
|
|
86
|
+
2. Inspect for a uniqueness `detail` on a `source.pointer`
|
|
87
|
+
that names the uniqueness key (usually `/name` or `/slug`).
|
|
88
|
+
3. Treat as "already created" and fetch the existing row.
|
|
89
|
+
|
|
90
|
+
The integration test suite formalizes this as the
|
|
91
|
+
"idempotent retry path" pattern in places where local state
|
|
92
|
+
may have diverged from server state (e.g. a prior run wrote
|
|
93
|
+
to the server but failed to capture the response locally).
|
|
94
|
+
|
|
95
|
+
## Polling cost
|
|
96
|
+
|
|
97
|
+
Polling at 1s intervals against the platform's
|
|
98
|
+
`/<resource>/:slug` endpoint is cheap for most resources but
|
|
99
|
+
becomes expensive at high concurrency. For long-running
|
|
100
|
+
operations (lambda deployments, large data-activation-client
|
|
101
|
+
runs), back off the interval (e.g. 2s → 5s → 10s) after the
|
|
102
|
+
first ~30s of waiting.
|
|
103
|
+
|
|
104
|
+
## What the SDK does NOT auto-poll
|
|
105
|
+
|
|
106
|
+
The typed client returns the immediate POST response — it
|
|
107
|
+
does NOT block until the resource reaches terminal status.
|
|
108
|
+
Consumers explicitly write the wait loop above where it
|
|
109
|
+
matters. This is intentional: not every caller wants to
|
|
110
|
+
block (some kick off async work and return; some need the
|
|
111
|
+
ready signal before downstream).
|