@alvera-ai/platform-sdk 0.19.2 → 0.20.0-next.g8dc7938
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 +10 -8
- package/.agent/account_management.md +14 -13
- package/.agent/action_status_updaters.md +5 -4
- package/.agent/ai_sandbox.md +3 -3
- package/.agent/authenticated_links.md +247 -0
- package/.agent/data_activation_clients.md +107 -4
- package/.agent/datalakes.md +1 -1
- package/.agent/interoperability_contracts.md +1 -1
- package/.agent/mdm.md +4 -4
- package/.agent/messages.md +9 -3
- package/.agent/mock-services.md +4 -5
- package/.agent/mutations.md +4 -4
- package/.agent/tool-call-configs.md +2 -2
- package/.agent/tools.md +10 -3
- package/.agent/type_naming.md +3 -4
- package/.agent/workflows.md +45 -41
- package/README.md +1 -1
- package/dist/index.d.mts +394 -606
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +124 -327
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/.agent/connected_apps.md +0 -525
package/.agent/AGENTS.md
CHANGED
|
@@ -93,7 +93,7 @@ corpus page documents the surface you already know:
|
|
|
93
93
|
| Sampling | Dataset event triggers | `data_activation_clients.md` §6.4 logs |
|
|
94
94
|
| Tools | Tool Calls (polymorphic body)| `tools.md` |
|
|
95
95
|
| Roots | Datalake boundaries | `datalakes.md` |
|
|
96
|
-
| Human-in-the-loop |
|
|
96
|
+
| Human-in-the-loop | Authenticated links | `authenticated_links.md` |
|
|
97
97
|
| Sandboxed execution | Liquid + SQL fragment lanes | `ai_sandbox.md` |
|
|
98
98
|
|
|
99
99
|
A few caveats on the mapping:
|
|
@@ -204,9 +204,9 @@ admin/bootstrap flows (signup, confirmation, tenant creation,
|
|
|
204
204
|
test-suite setup) — they are NOT app-building surfaces; an app that
|
|
205
205
|
holds a root or tenantless Bearer is misdesigned. An app
|
|
206
206
|
authenticates against exactly one tenant with that tenant's
|
|
207
|
-
publishable key. Likewise,
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
publishable key. Likewise, the host behind an action's
|
|
208
|
+
`link_prefix` resolves its links against one tenant, on that tenant's
|
|
209
|
+
publishable key (see `authenticated_links.md`).
|
|
210
210
|
|
|
211
211
|
Consumers holding multiple concurrent clients in the same file
|
|
212
212
|
(e.g. integration tests that need root + tenantless + tenant-
|
|
@@ -248,7 +248,6 @@ numbered steps inside the cookbook whose use case motivates it.
|
|
|
248
248
|
consumed by other resources.
|
|
249
249
|
ai_agents.md LLM-backed workers.
|
|
250
250
|
action_status_updaters.md Polling reconcilers.
|
|
251
|
-
connected_apps.md External app bridges.
|
|
252
251
|
mdm.md Entity resolution + identity
|
|
253
252
|
verification across domains.
|
|
254
253
|
ai_sandbox.md Three-layer safety contract
|
|
@@ -279,13 +278,16 @@ numbered steps inside the cookbook whose use case motivates it.
|
|
|
279
278
|
action_logs.md The action_logs DATASET — per-
|
|
280
279
|
action outcomes, batch_id joins,
|
|
281
280
|
audience export.
|
|
281
|
+
authenticated_links.md Every URL a message carries, as
|
|
282
|
+
<link_prefix>/<token>; the host
|
|
283
|
+
behind the prefix resolves taps
|
|
284
|
+
and reports the page.
|
|
282
285
|
```
|
|
283
286
|
|
|
284
287
|
Axes are mostly disjoint but a few resources cross. AI Agents
|
|
285
288
|
anchor INFRASTRUCTURE and appear in DATA ACTIVATION (they
|
|
286
289
|
tokenize during ingestion) and WORKFLOWS (workflows invoke
|
|
287
|
-
them).
|
|
288
|
-
WORKFLOWS.
|
|
290
|
+
them).
|
|
289
291
|
|
|
290
292
|
Each resource is exposed via a TypeScript namespace on the
|
|
291
293
|
typed client. The SDK namespace name follows TS conventions
|
|
@@ -299,13 +301,13 @@ wire name (snake_case):
|
|
|
299
301
|
api.tools tools.md
|
|
300
302
|
api.aiAgents ai_agents.md
|
|
301
303
|
api.actionStatusUpdaters action_status_updaters.md
|
|
302
|
-
api.connectedApps connected_apps.md
|
|
303
304
|
api.mdm mdm.md
|
|
304
305
|
api.genericTables generic_tables.md
|
|
305
306
|
api.advancedMigrations advanced_migrations.md
|
|
306
307
|
api.interoperabilityContracts interoperability_contracts.md
|
|
307
308
|
api.dataActivationClients data_activation_clients.md
|
|
308
309
|
api.workflows workflows.md
|
|
310
|
+
api.authenticatedLinks authenticated_links.md
|
|
309
311
|
|
|
310
312
|
Two corpus pages document DATASETS, not namespaces — read
|
|
311
313
|
through `api.datasets` with SINGULAR dataset types (the plural
|
|
@@ -99,15 +99,16 @@ a tenant (creator path) or accept an invitation (invitee path).
|
|
|
99
99
|
are platform-UI / admin-bootstrap surfaces (signup, confirmation,
|
|
100
100
|
tenant creation, test setup) — never ship them inside an app. An
|
|
101
101
|
app authenticates **tenant-scoped against exactly one tenant**,
|
|
102
|
-
with that tenant's publishable key;
|
|
103
|
-
|
|
102
|
+
with that tenant's publishable key; the host behind an action's
|
|
103
|
+
`link_prefix` in particular resolves links for one tenant
|
|
104
|
+
(`authenticated_links.md`).
|
|
104
105
|
|
|
105
106
|
### Key-only clients — machine-to-machine on the publishable key
|
|
106
107
|
|
|
107
|
-
`sessionToken` is optional on the client config.
|
|
108
|
-
|
|
109
|
-
key constructs with just the key — no session mint, no
|
|
110
|
-
empty token:
|
|
108
|
+
`sessionToken` is optional on the client config. The host behind an
|
|
109
|
+
action's `link_prefix`, authenticating purely on its tenant's
|
|
110
|
+
publishable key, constructs with just the key — no session mint, no
|
|
111
|
+
fabricated empty token:
|
|
111
112
|
|
|
112
113
|
```typescript
|
|
113
114
|
const api = createIsolatedPlatformApi({ baseUrl, apiKey })
|
|
@@ -118,15 +119,15 @@ and reaches exactly the `public_api` (publishable-key) allowlist —
|
|
|
118
119
|
five calls:
|
|
119
120
|
|
|
120
121
|
```
|
|
121
|
-
api.
|
|
122
|
-
api.
|
|
123
|
-
api.dataActivationClients.ingest
|
|
124
|
-
api.dataActivationClients.ingestFile
|
|
125
|
-
api.mdm.verify
|
|
122
|
+
api.authenticatedLinks.exchange a tap on a link
|
|
123
|
+
api.authenticatedLinks.updateMessageTracking opened / form_submitted
|
|
124
|
+
api.dataActivationClients.ingest submit rows
|
|
125
|
+
api.dataActivationClients.ingestFile submit a file
|
|
126
|
+
api.mdm.verify resolve an identity
|
|
126
127
|
```
|
|
127
128
|
|
|
128
|
-
(
|
|
129
|
-
|
|
129
|
+
(`authenticated_links.md` owns the details of the two link calls.)
|
|
130
|
+
Every call outside these five returns
|
|
130
131
|
401; that ceiling is the point, not an error to work around.
|
|
131
132
|
Minting a session and passing `sessionToken` unlocks the full
|
|
132
133
|
tenant-scoped API exactly as before — the two credentials are
|
|
@@ -635,9 +635,10 @@ is the part that matters:
|
|
|
635
635
|
decision (`mode: 'live'`, `manual_override: true`) — the same
|
|
636
636
|
call as any manual dispatch; there is no retry endpoint. See
|
|
637
637
|
`workflows.md` §8.
|
|
638
|
-
- **Engagement is not here either.** `
|
|
639
|
-
`
|
|
640
|
-
`
|
|
641
|
-
|
|
638
|
+
- **Engagement is not here either.** `clicked` arrives via
|
|
639
|
+
`authenticatedLinks.exchange`, and `opened` / `form_submitted`
|
|
640
|
+
with their times via `authenticatedLinks.updateMessageTracking`
|
|
641
|
+
— both called by the host behind the action's `link_prefix`
|
|
642
|
+
(`authenticated_links.md`).
|
|
642
643
|
- Field-by-field semantics of the rows being updated:
|
|
643
644
|
`messages.md`, `action_logs.md`.
|
package/.agent/ai_sandbox.md
CHANGED
|
@@ -157,7 +157,7 @@ trail this trap leaves.
|
|
|
157
157
|
|
|
158
158
|
SQL is the second consumer-author surface: WHERE clauses on
|
|
159
159
|
dataset searches (`api.datasets.createUserSearch.search_query`,
|
|
160
|
-
see `data_activation_clients.md` §6.
|
|
160
|
+
see `data_activation_clients.md` §6.6). Liquid renders some of
|
|
161
161
|
these from templates; others arrive directly as raw SQL
|
|
162
162
|
fragments. Either path lands at the same boundary.
|
|
163
163
|
|
|
@@ -165,7 +165,7 @@ fragments. Either path lands at the same boundary.
|
|
|
165
165
|
|
|
166
166
|
- SELECT against the tables exposed by the dataset search aliases
|
|
167
167
|
(`le` for legal entities, `m` for messages, `al` for action logs
|
|
168
|
-
— see `data_activation_clients.md` §6.
|
|
168
|
+
— see `data_activation_clients.md` §6.6 SQL aliases).
|
|
169
169
|
- Filter results into the platform's `search_results`
|
|
170
170
|
accumulator scoped to the materialised search id. This is the
|
|
171
171
|
one write the query path performs, and the platform builds it
|
|
@@ -305,5 +305,5 @@ guarantees it captures back-stop Layer 1.
|
|
|
305
305
|
- `account_management.md` §3 — session role vocabularies (Layer 1)
|
|
306
306
|
- `interoperability_contracts.md` §2 — template_config body shape (Layer 2)
|
|
307
307
|
- `workflows.md` gotcha 9 — the `| to_json` vs `| json` trap (Layer 2)
|
|
308
|
-
- `data_activation_clients.md` §6.
|
|
308
|
+
- `data_activation_clients.md` §6.6 — dataset search SQL aliases (Layer 3)
|
|
309
309
|
- `mdm.md` — verification surface that composes with Layer 1
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Authenticated links
|
|
2
|
+
|
|
3
|
+
Every URL in an SMS or MMS body, and every `href` in an email body, goes out
|
|
4
|
+
as an **authenticated link**:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
<link_prefix>/<token>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`link_prefix` is set on the action's tool call. It is the address of a host
|
|
11
|
+
**you** run. The platform appends only the token; any path belongs to the
|
|
12
|
+
prefix. When a person taps the link, the request reaches your host, and your
|
|
13
|
+
host asks the platform what the link is with one call and reports what the
|
|
14
|
+
person did on the page with a second.
|
|
15
|
+
|
|
16
|
+
SDK namespace: `api.authenticatedLinks` — `exchange` and
|
|
17
|
+
`updateMessageTracking`.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
workflow action (sms_request · mms_request · email_request)
|
|
21
|
+
link_prefix = "https://go.example.com/l"
|
|
22
|
+
body = "Book a slot: https://booking.example.com/slots"
|
|
23
|
+
│ send
|
|
24
|
+
▼
|
|
25
|
+
delivered "Book a slot: https://go.example.com/l/q3v9RkZ0aB7xYtL"
|
|
26
|
+
│ a person taps
|
|
27
|
+
▼
|
|
28
|
+
your host at go.example.com/l
|
|
29
|
+
exchange({ token, method, user_agent, ip, country, headers })
|
|
30
|
+
→ destination_url "https://booking.example.com/slots",
|
|
31
|
+
legal_entity_id, workflow, run, action, expires_at, message
|
|
32
|
+
send the person on to destination_url
|
|
33
|
+
│ the page opens · the form is submitted
|
|
34
|
+
▼
|
|
35
|
+
updateMessageTracking({ token, status: 'opened', opened_at })
|
|
36
|
+
updateMessageTracking({ token, status: 'form_submitted', form_submitted_at })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Both calls need only the tenant's publishable key.** The host is a key-only
|
|
40
|
+
client: no session, no user.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { createIsolatedPlatformApi } from '@alvera-ai/platform-sdk'
|
|
44
|
+
|
|
45
|
+
const links = createIsolatedPlatformApi({
|
|
46
|
+
baseUrl: 'https://api.alvera.ai',
|
|
47
|
+
apiKey: 'pk_live_tenant_publishable_key',
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This replaces Connected Apps and page tokens, which the platform retired:
|
|
52
|
+
there is no app to register, no route discovery, no `resolvePage`, and no
|
|
53
|
+
`{{ connected_app_form_url }}`. Write the real destination URL into the
|
|
54
|
+
message body; the platform turns it into an authenticated link at send.
|
|
55
|
+
|
|
56
|
+
## 1. Wire shape
|
|
57
|
+
|
|
58
|
+
### On the action — set when you build the workflow
|
|
59
|
+
|
|
60
|
+
| Field | Where | Shape |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `link_prefix` | `SmsCallRequest`, `MmsCallRequest`, `EmailCallRequest` | **required** string — the address of your host |
|
|
63
|
+
| `link_validity_days` | the same three | optional whole number, at least 1, default **365** — how many days each link resolves after the send |
|
|
64
|
+
| `metadata_template` | every action type | optional Liquid template rendered to JSON once per send |
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import type { AgenticWorkflowRequestWritable } from '@alvera-ai/platform-sdk'
|
|
68
|
+
|
|
69
|
+
type Action = NonNullable<AgenticWorkflowRequestWritable['actions']>[number]
|
|
70
|
+
|
|
71
|
+
const reviewRequest: Action['tool_call'] = {
|
|
72
|
+
tool_call_type: 'sms_request',
|
|
73
|
+
sms_type: 'transactional',
|
|
74
|
+
to: { type: 'custom', body: '{{ event_dataset.phone }}' },
|
|
75
|
+
body: {
|
|
76
|
+
type: 'custom',
|
|
77
|
+
body: 'Thanks for visiting. Tell us how it went: https://reviews.example.com/visit',
|
|
78
|
+
},
|
|
79
|
+
link_prefix: 'https://go.example.com/l',
|
|
80
|
+
link_validity_days: 30,
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `exchange` — a tap on the link
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import type { AuthenticatedLinkExchangeRequest } from '@alvera-ai/platform-sdk'
|
|
88
|
+
|
|
89
|
+
const tap: AuthenticatedLinkExchangeRequest = {
|
|
90
|
+
token: 'q3v9RkZ0aB7xYtL', // required: from <link_prefix>/<token>
|
|
91
|
+
method: 'GET', // the tap's HTTP method
|
|
92
|
+
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)',
|
|
93
|
+
ip: '203.0.113.7',
|
|
94
|
+
country: 'US',
|
|
95
|
+
headers: { 'accept-language': 'en-US' },
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `updateMessageTracking` — what happened on the page
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import type { AuthenticatedLinkUpdateMessageTrackingRequest } from '@alvera-ai/platform-sdk'
|
|
103
|
+
|
|
104
|
+
const submitted: AuthenticatedLinkUpdateMessageTrackingRequest = {
|
|
105
|
+
token: 'q3v9RkZ0aB7xYtL', // required: the same token
|
|
106
|
+
status: 'form_submitted', // typically 'opened' or 'form_submitted'
|
|
107
|
+
form_submitted_at: '2026-10-04T10:00:00Z',
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### The answer — `AuthenticatedLinkResponse`, from both calls
|
|
112
|
+
|
|
113
|
+
| Field | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `destination_url` | the URL as written in the message |
|
|
116
|
+
| `legal_entity_id` | the recipient's legal entity, when MDM resolved one — else `null` |
|
|
117
|
+
| `message_id` | the message the link went out in |
|
|
118
|
+
| `workflow_id` | the workflow that sent it |
|
|
119
|
+
| `workflow_execution_log_id` | the workflow run that sent it |
|
|
120
|
+
| `action_id` | the action that sent it |
|
|
121
|
+
| `expires_at` | when the link stops resolving |
|
|
122
|
+
| `link_clicked_at` | when a person first clicked this link — `null` until then |
|
|
123
|
+
| `message` | the message, a `MessageResponse` |
|
|
124
|
+
|
|
125
|
+
`message.metadata` holds the recipient's `to` merged with what the action's
|
|
126
|
+
`metadata_template` rendered, as stored at send. The message carries each
|
|
127
|
+
engagement status with its own time: `read` (`read_at`), `opened`
|
|
128
|
+
(`opened_at`), `clicked` (`clicked_at`) and `form_submitted`
|
|
129
|
+
(`form_submitted_at`).
|
|
130
|
+
|
|
131
|
+
## 2. Rules the type cannot encode
|
|
132
|
+
|
|
133
|
+
### `link_prefix` is an absolute URL with nothing after the path
|
|
134
|
+
|
|
135
|
+
An absolute `http` or `https` URL. A path is allowed —
|
|
136
|
+
`https://go.example.com/l` — but no query, no fragment and **no trailing
|
|
137
|
+
slash**: the platform appends `/<token>` itself.
|
|
138
|
+
|
|
139
|
+
### A person's tap moves the message to `clicked`; a machine's does not
|
|
140
|
+
|
|
141
|
+
`exchange` answers every tap, but only a person's moves anything. The first
|
|
142
|
+
click stamps the link's `link_clicked_at` and the message's `clicked_at`;
|
|
143
|
+
later taps keep both. The platform judges a tap from what your host forwards,
|
|
144
|
+
and these look automated and move nothing:
|
|
145
|
+
|
|
146
|
+
- a `HEAD` request;
|
|
147
|
+
- a prefetch header — `sec-purpose`, `purpose`, `x-purpose`, `x-moz`;
|
|
148
|
+
- an empty user agent, or one naming a bot, a link preview or a mail scanner.
|
|
149
|
+
|
|
150
|
+
Every tap is recorded with the details you forwarded and the signal that
|
|
151
|
+
flagged it, if one did. Headers are recorded as client headers — never cookies
|
|
152
|
+
or credentials.
|
|
153
|
+
|
|
154
|
+
### The message status only moves forward
|
|
155
|
+
|
|
156
|
+
`updateMessageTracking` applies its `status` through the message's order
|
|
157
|
+
guard: reporting `opened` after `form_submitted` leaves the message at
|
|
158
|
+
`form_submitted`. A time you leave empty keeps the message's value. The
|
|
159
|
+
`status` field is a plain string on the type; one a message cannot have is
|
|
160
|
+
refused with `422`.
|
|
161
|
+
|
|
162
|
+
### A link answers until it expires
|
|
163
|
+
|
|
164
|
+
Each link resolves for `link_validity_days` after the send, then answers
|
|
165
|
+
`410`. The response's `expires_at` says when.
|
|
166
|
+
|
|
167
|
+
### `metadata_template` renders once, at send
|
|
168
|
+
|
|
169
|
+
It renders to JSON once per send, on every action type, and is stored on the
|
|
170
|
+
message. A template that does not render **fails the send**. You read the
|
|
171
|
+
result back from `message.metadata` — not from a field on the link.
|
|
172
|
+
|
|
173
|
+
## 3. Field ownership
|
|
174
|
+
|
|
175
|
+
| Field | Written by |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `link_prefix`, `link_validity_days`, `metadata_template` | you, on the action |
|
|
178
|
+
| `destination_url` | you, as the URL written in the body |
|
|
179
|
+
| the token, `expires_at`, `message_id`, `workflow_id`, `workflow_execution_log_id`, `action_id` | the platform, at send |
|
|
180
|
+
| `legal_entity_id` | the platform, when MDM resolves the recipient |
|
|
181
|
+
| `link_clicked_at`, `message.clicked_at` | the platform, on a person's first tap |
|
|
182
|
+
| `message.opened_at`, `message.form_submitted_at` | your host, through `updateMessageTracking` |
|
|
183
|
+
|
|
184
|
+
## 4. Error envelopes
|
|
185
|
+
|
|
186
|
+
Both calls throw on a refusal, and the thrown value carries the HTTP status
|
|
187
|
+
as `_httpStatus` (see `errors.md`). They refuse in ways a host must tell
|
|
188
|
+
apart:
|
|
189
|
+
|
|
190
|
+
| Status | `exchange` | `updateMessageTracking` | Meaning |
|
|
191
|
+
|---|---|---|---|
|
|
192
|
+
| `404` | yes | yes | a token the platform never minted |
|
|
193
|
+
| `410` | yes | yes | the link is past `expires_at` |
|
|
194
|
+
| `503` | yes | yes | the datalake cannot be reached right now |
|
|
195
|
+
| `422` | — | yes | a status the message cannot have |
|
|
196
|
+
| `401` | yes | yes | no publishable key, or one that does not resolve |
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
async function resolveTap(token: string, userAgent: string): Promise<string> {
|
|
200
|
+
try {
|
|
201
|
+
const { data } = await api.authenticatedLinks.exchange(tenantSlug, datalakeSlug, {
|
|
202
|
+
token,
|
|
203
|
+
method: 'GET',
|
|
204
|
+
user_agent: userAgent,
|
|
205
|
+
})
|
|
206
|
+
return data.destination_url
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const status = (err as { _httpStatus?: number })._httpStatus
|
|
209
|
+
if (status === 410) return 'https://go.example.com/expired'
|
|
210
|
+
if (status === 404) return 'https://go.example.com/not-found'
|
|
211
|
+
if (status === 503) return 'https://go.example.com/try-again'
|
|
212
|
+
throw err
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## 5. Lifecycle
|
|
218
|
+
|
|
219
|
+
1. **Build.** Give every SMS, MMS and email action a `link_prefix`, and a
|
|
220
|
+
`metadata_template` when your page needs more than the recipient.
|
|
221
|
+
2. **Send.** Each URL in the body becomes `<link_prefix>/<token>`, valid for
|
|
222
|
+
`link_validity_days`.
|
|
223
|
+
3. **Tap.** Your host calls `exchange` with the token and the tap's request
|
|
224
|
+
details, and sends the person on to `destination_url`. A person's tap
|
|
225
|
+
moves the message to `clicked`.
|
|
226
|
+
4. **Page.** Your host reports `opened` when its page opens and
|
|
227
|
+
`form_submitted` when the form is done, each with its time.
|
|
228
|
+
5. **Expiry.** After `expires_at` both calls answer `410`.
|
|
229
|
+
|
|
230
|
+
## 6. Gotchas
|
|
231
|
+
|
|
232
|
+
- **Forward what the tap said about itself.** Only the token is required, but
|
|
233
|
+
a host that forwards nothing else gives the platform nothing to judge a
|
|
234
|
+
person by. Pass the method, user agent and headers of the request that
|
|
235
|
+
reached you, not those of your own server-side call.
|
|
236
|
+
- **Link previews are taps too.** Messaging apps fetch a link to draw its
|
|
237
|
+
preview; those are recorded and do not count as clicks.
|
|
238
|
+
- **Put the path in the prefix.** The platform appends only the token, so
|
|
239
|
+
`https://go.example.com/l` gives `https://go.example.com/l/<token>`. A
|
|
240
|
+
trailing slash is refused.
|
|
241
|
+
- **`metadata` lives on the message.** Read `data.message.metadata`; the link
|
|
242
|
+
itself has no metadata field.
|
|
243
|
+
- **Coming from Connected Apps.** `connected_app_id`, `connected_app_route`
|
|
244
|
+
and `connected_app_metadata_template` are gone from actions;
|
|
245
|
+
`link_metadata_template` is now `metadata_template`; the body carries the
|
|
246
|
+
destination URL itself instead of `{{ connected_app_form_url }}`; and
|
|
247
|
+
`updateMessageTracking` is keyed by the link's token, not a page token.
|
|
@@ -377,7 +377,7 @@ Three run shapes exist, selected by the bound tool's
|
|
|
377
377
|
All three produce **log rows** observable through
|
|
378
378
|
`api.dataActivationClients.logs.*` (§6.4), and the canonical way
|
|
379
379
|
to **verify** that ingestion worked is the two-step
|
|
380
|
-
dataset-search pattern in §6.
|
|
380
|
+
dataset-search pattern in §6.6.
|
|
381
381
|
|
|
382
382
|
### 6.1 Inline JSON ingestion — `.ingest`
|
|
383
383
|
|
|
@@ -397,7 +397,7 @@ const { data } = await api.dataActivationClients.ingest(
|
|
|
397
397
|
The response returns **immediately after enqueueing**, not
|
|
398
398
|
after ingestion completes. To know when the row has actually
|
|
399
399
|
been written, poll the logs (§6.4) or use the verification
|
|
400
|
-
pattern (§6.
|
|
400
|
+
pattern (§6.6).
|
|
401
401
|
|
|
402
402
|
The `batch_id` is the most important field on the response —
|
|
403
403
|
every downstream log row and dataset row carries the same
|
|
@@ -534,7 +534,110 @@ merge step has committed the ndjson archive. Use
|
|
|
534
534
|
`output_files.length > 0` as the "this batch's rows are fully
|
|
535
535
|
durable" signal — not just the presence of the log row.
|
|
536
536
|
|
|
537
|
-
### 6.5
|
|
537
|
+
### 6.5 Runs subresource — `.batches.list` / `.batches.get`
|
|
538
|
+
|
|
539
|
+
A **run** is one record; the log rows of §6.4 are one record per
|
|
540
|
+
dataset table that run wrote into. Read the run to answer *"is this
|
|
541
|
+
over, and how did it end"*; read the logs for the per-table detail,
|
|
542
|
+
filtered on the same `batch_id`.
|
|
543
|
+
|
|
544
|
+
Before this record existed, the only way to ask whether a run had
|
|
545
|
+
finished was to count log rows and guess — which cannot tell a run
|
|
546
|
+
still going from one that produced nothing and never will. A run
|
|
547
|
+
that was skipped or that died looked exactly like a slow one until
|
|
548
|
+
the caller's timeout expired.
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
batch_id UUID — the run identifier, the same one `.ingest`,
|
|
552
|
+
`.ingestFile` and `.runManually` hand back, and the
|
|
553
|
+
one stamped on every per-table log row of this run
|
|
554
|
+
client_id UUID — the owning activation client
|
|
555
|
+
closed_at ISO timestamp | null — when the run was attempted and
|
|
556
|
+
exhausted. Null while it is still going. It does NOT
|
|
557
|
+
mean rows landed and it does NOT mean the archive was
|
|
558
|
+
merged — read `status` for the outcome, and only once
|
|
559
|
+
this is set.
|
|
560
|
+
status 'processing' | 'succeeded' | 'partial' | 'skipped' | 'failed'
|
|
561
|
+
rows_total number — source rows enqueued for this run. The run's own
|
|
562
|
+
count; each per-table log keeps its own.
|
|
563
|
+
inserted_at ISO timestamp — when the run started
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**`status` means nothing until `closed_at` is set.** A run in flight
|
|
567
|
+
always reads `processing`. Once it is closed:
|
|
568
|
+
|
|
569
|
+
| status | what happened |
|
|
570
|
+
|---|---|
|
|
571
|
+
| `succeeded` | it ran and nothing was refused |
|
|
572
|
+
| `partial` | it ran and at least one row was refused on the way in; the rows that landed are still there |
|
|
573
|
+
| `skipped` | it ran and there was nothing to do — empty file, every row filtered out, or the client has no contracts |
|
|
574
|
+
| `failed` | it died before writing anything |
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// Paginated list of runs for an activation client. Takes the full Flop
|
|
578
|
+
// query (page / page_size / order_by / order_directions / filters —
|
|
579
|
+
// type_naming.md); without it, page 1 only.
|
|
580
|
+
const { data } = await api.dataActivationClients.batches.list(
|
|
581
|
+
tenantSlug, datalakeSlug, activationClientSlug,
|
|
582
|
+
{ page: 1, page_size: 50, order_by: ['inserted_at'], order_directions: ['desc'] },
|
|
583
|
+
)
|
|
584
|
+
// data.data — array of run records; data.meta — pagination
|
|
585
|
+
|
|
586
|
+
// Fetch one run by its batch_id.
|
|
587
|
+
const { data: run } = await api.dataActivationClients.batches.get(
|
|
588
|
+
tenantSlug, datalakeSlug, activationClientSlug, batchId,
|
|
589
|
+
)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
> **The fourth argument is the `batch_id`, not the record's `id`.**
|
|
593
|
+
> A run record carries both, and both are UUIDs. The route looks the
|
|
594
|
+
> run up by its `batch_id` — the identifier the ingest handed you — so
|
|
595
|
+
> passing the record's own `id` is a **404**, not a type error.
|
|
596
|
+
|
|
597
|
+
#### Waiting on a run
|
|
598
|
+
|
|
599
|
+
This is what the run record is for. Poll until `closed_at` is set,
|
|
600
|
+
then branch on `status` — never poll `status` alone, because a run
|
|
601
|
+
in flight reads `processing` forever and tells you nothing:
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
const { data: ingest } = await api.dataActivationClients.ingest(
|
|
605
|
+
tenantSlug, datalakeSlug, activationClientSlug, { data: row },
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
const deadline = Date.now() + 120_000
|
|
609
|
+
let run
|
|
610
|
+
for (;;) {
|
|
611
|
+
const { data } = await api.dataActivationClients.batches.get(
|
|
612
|
+
tenantSlug, datalakeSlug, activationClientSlug, ingest.batch_id,
|
|
613
|
+
)
|
|
614
|
+
if (data.closed_at != null) { run = data; break }
|
|
615
|
+
if (Date.now() > deadline) throw new Error(`run ${ingest.batch_id} never closed`)
|
|
616
|
+
await new Promise((r) => setTimeout(r, 1_000))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (run.status === 'failed') {
|
|
620
|
+
throw new Error(`run ${ingest.batch_id} failed before writing anything`)
|
|
621
|
+
}
|
|
622
|
+
if (run.status === 'skipped') {
|
|
623
|
+
throw new Error(`run ${ingest.batch_id} had nothing to do — check the row filter and the client's contracts`)
|
|
624
|
+
}
|
|
625
|
+
// 'succeeded' or 'partial' — the run is over and rows were written.
|
|
626
|
+
// For a partial, read the log rows (§6.4) for which table refused what.
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
A run that failed or was skipped **ends the wait immediately, carrying
|
|
630
|
+
why**. That is the whole difference from counting rows: a blind poll
|
|
631
|
+
spends its full timeout and then reports nothing about the cause.
|
|
632
|
+
|
|
633
|
+
> **`closed_at` is not the same as "my row is queryable".** It says the
|
|
634
|
+
> run is over. It does not say the archive was merged (that is
|
|
635
|
+
> `output_files` on the log rows, §6.4), and for one specific row it
|
|
636
|
+
> does not replace the landing check in §6.6 — it makes that check a
|
|
637
|
+
> single read after the run is known finished, instead of a guess
|
|
638
|
+
> against a deadline.
|
|
639
|
+
|
|
640
|
+
### 6.6 Verifying ingestion: the two-step dataset search
|
|
538
641
|
|
|
539
642
|
> **Simplest landing check — `executeSql`.** Ingestion is async (the row
|
|
540
643
|
> commits a moment after the `202`). When you just need to know a row
|
|
@@ -701,7 +804,7 @@ screen. Pick the mode by which of those two readers you are serving.
|
|
|
701
804
|
response carries `batch_id` + `jobs_count`, signalling that
|
|
702
805
|
downstream jobs have been enqueued — not that they've
|
|
703
806
|
completed. Poll logs (or use the search verification
|
|
704
|
-
pattern in §6.
|
|
807
|
+
pattern in §6.6) before treating the row as ingested.
|
|
705
808
|
|
|
706
809
|
7. **`dataset_updated > 0` is the load-bearing freshness
|
|
707
810
|
gate.** `rows_ingested` flips as soon as the per-row job
|
package/.agent/datalakes.md
CHANGED
|
@@ -110,7 +110,7 @@ The platform does NOT expose a query API over the archive itself —
|
|
|
110
110
|
cold reads happen externally so the platform's processes aren't
|
|
111
111
|
responsible for analytical workloads. Live data lives in the
|
|
112
112
|
datalake's Postgres halves (queryable via the dataset search
|
|
113
|
-
two-step pattern in `data_activation_clients.md` §6.
|
|
113
|
+
two-step pattern in `data_activation_clients.md` §6.6); historical
|
|
114
114
|
data lives in the cloud-storage buckets configured here.
|
|
115
115
|
|
|
116
116
|
### Canonical create body
|
|
@@ -299,7 +299,7 @@ in the datalake and there is nothing to search afterwards. The
|
|
|
299
299
|
contract's `resource_type` destination) runs only when the DAC
|
|
300
300
|
worker ingests a row — i.e. on `dataActivationClients.ingest`, not
|
|
301
301
|
on `.run`. To prove a row *landed*, ingest and then query by
|
|
302
|
-
`batch_id` (see `data_activation_clients.md` §6.
|
|
302
|
+
`batch_id` (see `data_activation_clients.md` §6.6).
|
|
303
303
|
|
|
304
304
|
```typescript
|
|
305
305
|
const { data: result } = await api.interoperabilityContracts.run(
|
package/.agent/mdm.md
CHANGED
|
@@ -26,10 +26,10 @@ master records.
|
|
|
26
26
|
## 1. Wire shape — `.verify`
|
|
27
27
|
|
|
28
28
|
Verify that a person on the other end of a workflow action or a
|
|
29
|
-
|
|
29
|
+
form behind an authenticated link matches an existing subject in the
|
|
30
30
|
datalake. Used at action time (workflows confirming an SMS
|
|
31
|
-
recipient) and at runtime (
|
|
32
|
-
submitter before posting tracking updates).
|
|
31
|
+
recipient) and at runtime (the host behind `link_prefix` confirming
|
|
32
|
+
the form submitter before posting tracking updates).
|
|
33
33
|
|
|
34
34
|
Every MDM subject is ultimately a **person** or a **company**,
|
|
35
35
|
and the request type is that ontology's identity vocabulary. You
|
|
@@ -339,7 +339,7 @@ SDK surface:
|
|
|
339
339
|
|
|
340
340
|
Today there is no `.list`, `.get`, or `.metadata` on the MDM
|
|
341
341
|
namespace — subjects themselves are queried through the dataset
|
|
342
|
-
search surface (see `data_activation_clients.md` §6.
|
|
342
|
+
search surface (see `data_activation_clients.md` §6.6), not
|
|
343
343
|
through `api.mdm`.
|
|
344
344
|
|
|
345
345
|
## 6. Gotchas
|
package/.agent/messages.md
CHANGED
|
@@ -44,7 +44,12 @@ alias: `m`.
|
|
|
44
44
|
invalidated (idempotency key collision — the fire was
|
|
45
45
|
rejected, nothing went out)
|
|
46
46
|
received (inbound message, direction: 'inbound')
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
engagement, each with its own time:
|
|
49
|
+
read read_at
|
|
50
|
+
opened opened_at the link host's page opened
|
|
51
|
+
clicked clicked_at a person first tapped a link
|
|
52
|
+
form_submitted form_submitted_at the link host's form was submitted
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
`accepted` (the provider took the send request) and `sending`
|
|
@@ -53,13 +58,14 @@ and `sent`; the order above is the order the contract declares them.
|
|
|
53
58
|
Treat any of them as "in flight, outcome unknown" — see **Status is
|
|
54
59
|
eventually honest** below.
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
Four different writers touch a message row. Know which is which:
|
|
57
62
|
|
|
58
63
|
| Writer | Writes | When |
|
|
59
64
|
|---|---|---|
|
|
60
65
|
| the action executor | the row itself; `pending`→`sent`, `external_id`, `sent_at`; also `duplicate` | at execution (writes the row at `pending`; advances to `sent` + sets `external_id` only on a live tool call — a dry-run stops at `pending`). Lands `duplicate` when the provider id it got back is already held by another send through the same tool |
|
|
61
66
|
| the action status updater | `delivered` / `failed`, `delivered_at`, `failure_reason` | later, when its poll runs (see `action_status_updaters.md` §7) |
|
|
62
|
-
| `
|
|
67
|
+
| `authenticatedLinks.exchange` | `clicked`, `clicked_at` (the first person's tap only) | when the host behind `link_prefix` forwards a tap; automated taps move nothing |
|
|
68
|
+
| `authenticatedLinks.updateMessageTracking` | `opened` / `form_submitted`, `opened_at`, `form_submitted_at` | when the host reports its page opened or its form submitted; status only moves forward |
|
|
63
69
|
|
|
64
70
|
Two consequences:
|
|
65
71
|
|
package/.agent/mock-services.md
CHANGED
|
@@ -95,21 +95,21 @@ flowchart TD
|
|
|
95
95
|
end
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
**It lives in this repo**, at `cloudflare
|
|
98
|
+
**It lives in this repo**, at `cloudflare/workers/mock/`. Deploy from a normal checkout:
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
|
-
cd cloudflare
|
|
101
|
+
cd cloudflare/workers/mock && wrangler deploy
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
The single line that makes this work without a vendored copy of the corpus:
|
|
105
105
|
|
|
106
106
|
```jsonc
|
|
107
|
-
"image_build_context": "
|
|
107
|
+
"image_build_context": "../../.."
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
That sets the Docker build context to the repo root, so the Dockerfile copies
|
|
111
111
|
`wm_mappings/` and `wm_response_files/` from where they already live. Without it
|
|
112
|
-
the context is `cloudflare
|
|
112
|
+
the context is `cloudflare/workers/mock/` and the corpus would have to be duplicated there —
|
|
113
113
|
two directories that look identical until they quietly aren't.
|
|
114
114
|
|
|
115
115
|
### There is no authentication
|
|
@@ -190,4 +190,3 @@ flip.
|
|
|
190
190
|
|
|
191
191
|
- [Tools](tools.md) — tool bodies, `endpoint_url`, and REST API auth methods
|
|
192
192
|
- [Datalakes](datalakes.md) — how datalake provisioning uses MiniStack
|
|
193
|
-
- [Connected Apps](connected_apps.md) — the third-party integrations the corpus stands in for
|