@introspection-sdk/introspection-node 0.22.0 → 0.31.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/README.md +430 -8
- package/dist/auth-client.d.ts +182 -0
- package/dist/auth-client.d.ts.map +1 -0
- package/dist/auth-client.js +402 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/auth.d.ts +12 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +51 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +80 -6
- package/dist/client.js.map +1 -1
- package/dist/data-plane.d.ts +69 -0
- package/dist/data-plane.d.ts.map +1 -0
- package/dist/data-plane.js +61 -0
- package/dist/data-plane.js.map +1 -0
- package/dist/http.d.ts +23 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +26 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/index.d.ts +3 -1
- package/dist/otel/index.d.ts.map +1 -1
- package/dist/otel/index.js +2 -1
- package/dist/otel/index.js.map +1 -1
- package/dist/otel/init.d.ts +7 -2
- package/dist/otel/init.d.ts.map +1 -1
- package/dist/otel/init.js +9 -2
- package/dist/otel/init.js.map +1 -1
- package/dist/otel/logs.d.ts +23 -6
- package/dist/otel/logs.d.ts.map +1 -1
- package/dist/otel/logs.js +56 -13
- package/dist/otel/logs.js.map +1 -1
- package/dist/resources/annotations.d.ts +28 -0
- package/dist/resources/annotations.d.ts.map +1 -0
- package/dist/resources/annotations.js +198 -0
- package/dist/resources/annotations.js.map +1 -0
- package/dist/resources/app-connections.d.ts +35 -0
- package/dist/resources/app-connections.d.ts.map +1 -0
- package/dist/resources/app-connections.js +66 -0
- package/dist/resources/app-connections.js.map +1 -0
- package/dist/resources/automations.d.ts +5 -0
- package/dist/resources/automations.d.ts.map +1 -0
- package/dist/resources/automations.js +6 -0
- package/dist/resources/automations.js.map +1 -0
- package/dist/resources/connectors.d.ts +34 -2
- package/dist/resources/connectors.d.ts.map +1 -1
- package/dist/resources/connectors.js +53 -1
- package/dist/resources/connectors.js.map +1 -1
- package/dist/resources/experiments.d.ts +6 -6
- package/dist/resources/experiments.d.ts.map +1 -1
- package/dist/resources/experiments.js +29 -6
- package/dist/resources/experiments.js.map +1 -1
- package/dist/resources/members.d.ts +33 -0
- package/dist/resources/members.d.ts.map +1 -0
- package/dist/resources/members.js +59 -0
- package/dist/resources/members.js.map +1 -0
- package/dist/resources/repositories.d.ts +52 -0
- package/dist/resources/repositories.d.ts.map +1 -0
- package/dist/resources/repositories.js +112 -0
- package/dist/resources/repositories.js.map +1 -0
- package/dist/runner.d.ts +19 -5
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +18 -3
- package/dist/runner.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +21 -16
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @introspection-sdk/introspection-node
|
|
2
2
|
|
|
3
3
|
Node.js platform SDK for [Introspection](https://introspection.dev) — open runtimes,
|
|
4
|
-
drive tasks, and manage experiments, recipes,
|
|
4
|
+
drive tasks, and manage experiments, recipes, repositories, members,
|
|
5
|
+
automations, app connections, files, conversations, and shares.
|
|
5
6
|
|
|
6
7
|
## Install
|
|
7
8
|
|
|
@@ -54,6 +55,329 @@ teardown.
|
|
|
54
55
|
Interrupted runs resume through
|
|
55
56
|
`runner.tasks.runs.resume(taskId, { resume: entries })`.
|
|
56
57
|
|
|
58
|
+
`identity.metadata` sets `key: value` labels on the `customer` member the
|
|
59
|
+
identity names. It grants nothing, so unlike `identity.tags` (which seed a new
|
|
60
|
+
member only) it merges into an existing member too: asserted keys overwrite
|
|
61
|
+
keys of the same name and the rest are kept. Absent or `{}` changes nothing.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const runner = await client.runtimes("customer-agent").run({
|
|
65
|
+
identity: { user_id: "u_42", metadata: { plan: "enterprise" } },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Data Plane resources: client and runner
|
|
70
|
+
|
|
71
|
+
`IntrospectionClient` and `Runner` expose the same Data Plane namespaces, and
|
|
72
|
+
both implement the exported `DataPlaneResources` interface: `tasks` (with
|
|
73
|
+
`tasks.runs`), `files`, `conversations`, `events`, `metrics`, `shares`,
|
|
74
|
+
`automations` and `connections`. So does the `DataPlaneClient` that
|
|
75
|
+
`AuthClient.dataPlane()` returns. Code written against `DataPlaneResources`
|
|
76
|
+
runs against any of them.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import type { DataPlaneResources } from "@introspection-sdk/introspection-node";
|
|
80
|
+
|
|
81
|
+
async function connectedApps(dp: DataPlaneResources) {
|
|
82
|
+
return (await dp.connections.list()).records.map((c) => c.app);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await connectedApps(client); // the client's own token, on its Data Plane
|
|
86
|
+
await connectedApps(runner); // the runner's token, on the runner's Data Plane
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The difference is the credential. A runner sends its session token, scoped to
|
|
90
|
+
the runtime and identity it was opened for; the client sends its own token,
|
|
91
|
+
so the server decides per call what that credential may do. A runner a member
|
|
92
|
+
opens for themself carries `automations:read` and `automations:write`, so
|
|
93
|
+
`runner.automations` manages that member's automations (a platform change in
|
|
94
|
+
progress; until it ships those routes refuse a runner with a 403).
|
|
95
|
+
|
|
96
|
+
### Members
|
|
97
|
+
|
|
98
|
+
`client.members` lists, reads, creates and updates members on the Control
|
|
99
|
+
Plane's `/v1/members`; removing one is an org-admin action left to the CLI. A
|
|
100
|
+
member's `tags` are access-bearing (it receives every share whose `granted_tag`
|
|
101
|
+
it holds) and writable only with `members:manage`. A member can also still read
|
|
102
|
+
and write any file or task whose tags intersect its own, but that implicit tag
|
|
103
|
+
access is deprecated: share with a cohort through a tag share instead. Its `metadata` is a
|
|
104
|
+
map of string labels that grants nothing; keys are `[A-Za-z0-9_-]+`, values are
|
|
105
|
+
non-empty strings, at most 64 entries.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const ada = await client.members.create({
|
|
109
|
+
email: "ada@example.com",
|
|
110
|
+
name: "Ada Lovelace",
|
|
111
|
+
metadata: { plan: "enterprise" },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// `tag` and `metadata` narrow the list; metadata pairs are ANDed (at most 16).
|
|
115
|
+
for await (const member of client.members.list({
|
|
116
|
+
member_type: "customer",
|
|
117
|
+
metadata: { plan: "enterprise", region: "eu" },
|
|
118
|
+
})) {
|
|
119
|
+
console.log(member.id, member.metadata);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// PATCH replaces `tags` and `metadata` wholesale; `{}` clears metadata.
|
|
123
|
+
await client.members.update(ada.id, { metadata: { plan: "team" } });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Seeding `tags` on create needs `members:manage` as well as `members:write`.
|
|
127
|
+
|
|
128
|
+
### Automations
|
|
129
|
+
|
|
130
|
+
`client.automations` (and `dataPlaneClient.automations`) lists, reads,
|
|
131
|
+
creates, updates and deletes the project's automations on the Data Plane's
|
|
132
|
+
`/v1/automations`, and `trigger(id)` runs one now. Reading needs
|
|
133
|
+
`automations:read` and writing `automations:write`; each record's `can_manage`
|
|
134
|
+
says whether the caller may change it. An automation is a scheduled prompt (`kind: null`) or platform work
|
|
135
|
+
(`observation_synthesis`, `observation_clustering`, `project_check_in`). A
|
|
136
|
+
`cron` automation fires on `cron_schedule`, or on `metadata.cron_schedules` in
|
|
137
|
+
`metadata.timezone`; a one-off reminder is a `manual` automation with a future
|
|
138
|
+
`next_trigger_at`. Set `task_id` to post each firing into an existing task
|
|
139
|
+
instead of creating one.
|
|
140
|
+
|
|
141
|
+
The server serves these routes to administrators only today (a 403 otherwise).
|
|
142
|
+
introspection-cloud#3137, not yet released, opens them to members for their own
|
|
143
|
+
automations that post into one of their tasks; until it ships, the `task_id`
|
|
144
|
+
list filter is ignored by the server.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const reminder = await client.automations.create({
|
|
148
|
+
name: "Friday check-in",
|
|
149
|
+
trigger_type: "manual",
|
|
150
|
+
prompt: "How did the week go?",
|
|
151
|
+
runtime_group_id: runtimeGroupId,
|
|
152
|
+
task_id: taskId,
|
|
153
|
+
next_trigger_at: "2026-10-09T09:00:00Z",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Recurring: one or more cron schedules in a timezone.
|
|
157
|
+
await client.automations.create({
|
|
158
|
+
name: "Weekday digest",
|
|
159
|
+
trigger_type: "cron",
|
|
160
|
+
cron_schedule: "0 9 * * 1-5",
|
|
161
|
+
metadata: {
|
|
162
|
+
cron_schedules: ["0 9 * * 1-5"],
|
|
163
|
+
timezone: "America/Los_Angeles",
|
|
164
|
+
},
|
|
165
|
+
prompt: "Summarize yesterday's conversations.",
|
|
166
|
+
runtime_group_id: runtimeGroupId,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// PATCH sends only the fields you set; `metadata` replaces wholesale.
|
|
170
|
+
await client.automations.update(reminder.id, { enabled: false });
|
|
171
|
+
|
|
172
|
+
for await (const automation of client.automations.list({ task_id: taskId })) {
|
|
173
|
+
console.log(automation.id, automation.next_trigger_at, automation.can_manage);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { status, task_id } = await client.automations.trigger(reminder.id);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A firing clears a manual automation's slot; re-arm it with
|
|
180
|
+
`update(id, { next_trigger_at })`. A `cron` automation derives its own slot and
|
|
181
|
+
rejects one set by the client.
|
|
182
|
+
|
|
183
|
+
Each task-run trigger is recorded as an `introspection.automation.triggered`
|
|
184
|
+
event, and a scheduled slot that ran nothing as
|
|
185
|
+
`introspection.automation.skipped` (with a `reason` such as
|
|
186
|
+
`conditions_not_met` or `target_busy`; readable only with project-wide
|
|
187
|
+
telemetry access). Read them with `client.events.list`, filtered by
|
|
188
|
+
`automation_id` or `task_id`:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const page = await client.events.list({
|
|
192
|
+
event_name: "introspection.automation.triggered",
|
|
193
|
+
automation_id: reminder.id,
|
|
194
|
+
});
|
|
195
|
+
for (const ev of page.records)
|
|
196
|
+
console.log(ev.payload.task_id, ev.payload.posted);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Connections
|
|
200
|
+
|
|
201
|
+
`connections` lists, reads, creates and deletes the apps members connected for
|
|
202
|
+
themselves, on the Data Plane's `/v1/connections`. A caller who is not an
|
|
203
|
+
administrator always sees and manages only their own. Reads need
|
|
204
|
+
`connections:read`, `create` needs `connections:write` and `delete` needs
|
|
205
|
+
`connections:delete`. These are a different resource from the Control Plane's
|
|
206
|
+
`client.connectors.connections`, which are nested under a connector.
|
|
207
|
+
|
|
208
|
+
`create` returns a single-use connect page; send the member to its
|
|
209
|
+
`authorize_url`, and the connection exists once they finish there. On a runner,
|
|
210
|
+
`runtime` defaults to the runner's runtime group. On the client, pass it.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const { authorize_url } = await runner.connections.create({ app: "gmail" });
|
|
214
|
+
|
|
215
|
+
for await (const connection of runner.connections.list({ app: "gmail" })) {
|
|
216
|
+
console.log(connection.id, connection.account_name, connection.healthy);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Administrators can list another member's connections.
|
|
220
|
+
await client.connections.list({ member_id: memberId });
|
|
221
|
+
|
|
222
|
+
await runner.connections.delete(connectionId);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Native email-code sign-in
|
|
226
|
+
|
|
227
|
+
`AuthClient` signs in the end users of a `native` Application with emailed
|
|
228
|
+
one-time codes, shaped like the Supabase Auth client. The session belongs to a
|
|
229
|
+
`customer` member of the Application's organization, is scoped to one project,
|
|
230
|
+
and carries the Application's `allowed_scopes`.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { AuthClient } from "@introspection-sdk/introspection-node";
|
|
234
|
+
|
|
235
|
+
const auth = new AuthClient({
|
|
236
|
+
clientId: "intro_app_…", // a `native` Application
|
|
237
|
+
project: "my-project",
|
|
238
|
+
storage: sessionStore, // getItem / setItem / removeItem, sync or async
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await auth.signInWithOtp({ email });
|
|
242
|
+
await auth.verifyOtp({ email, token: code });
|
|
243
|
+
|
|
244
|
+
const dp = await auth.dataPlane();
|
|
245
|
+
const run = await dp.tasks.start({ prompt: "Hello", runtime_id: runtimeId });
|
|
246
|
+
console.log(await run.text());
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- **Codes are opaque.** A returning user's code is 6 digits; a new user's first
|
|
250
|
+
code is 6 characters of A–Z and 0–9. Do not restrict the input to digits.
|
|
251
|
+
A wrong, expired or reused code throws `ValidationError` with
|
|
252
|
+
`code === "invalid_grant"`; requesting codes too often throws
|
|
253
|
+
`RateLimitError` with `retryAfter`.
|
|
254
|
+
- **Refresh.** `getSession()` renews the session within `leewaySeconds`
|
|
255
|
+
(default 60) of expiry, and every request made through `auth.dataPlane()`
|
|
256
|
+
renews it once after a `401`; concurrent renewals share one refresh. A
|
|
257
|
+
refresh the server rejects signs the user out and throws
|
|
258
|
+
`AuthenticationError`; a network failure keeps the session.
|
|
259
|
+
- **Superseded responses.** A sign-in that resolves after a sign-out or a newer
|
|
260
|
+
sign-in rejects with an `AbortError` instead of overwriting the newer state.
|
|
261
|
+
- **Events.** `onAuthStateChange((event, session) => …)` reports
|
|
262
|
+
`INITIAL_SESSION`, `SIGNED_IN`, `TOKEN_REFRESHED` and `SIGNED_OUT`.
|
|
263
|
+
- **Data Plane only.** The token is a Data Plane credential, so the Control
|
|
264
|
+
Plane routes on `IntrospectionClient` (runtimes, connectors, members) reject
|
|
265
|
+
it. Pass the runtime as `runtime_id` when creating a task. `auth.dataPlane()`
|
|
266
|
+
returns a `DataPlaneClient`, which implements `DataPlaneResources`: `tasks`,
|
|
267
|
+
`files`, `conversations`, `shares`, `events`, `metrics`, `automations` and
|
|
268
|
+
`connections` (where `create` takes the `runtime`).
|
|
269
|
+
- **Sign-out** clears the local session, then revokes it with
|
|
270
|
+
`POST /v1/oauth/revoke`; the local session is gone even if revocation fails.
|
|
271
|
+
|
|
272
|
+
See [`examples/api/native-email-code.ts`](../../examples/api/native-email-code.ts).
|
|
273
|
+
Federated sign-in through your own IdP (a `jwks` Application) keeps its session
|
|
274
|
+
in that IdP's SDK; exchange its token with `tokenExchange()` instead.
|
|
275
|
+
|
|
276
|
+
### Repositories
|
|
277
|
+
|
|
278
|
+
`client.repositories` looks up a project's linked repositories on the Control
|
|
279
|
+
Plane and reads their contents through the Data Plane (`repositories:read`).
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const [repo] = await client.repositories.list({
|
|
283
|
+
project: "demo",
|
|
284
|
+
slug: "acme/agent",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Every entry of a directory; pages are followed automatically.
|
|
288
|
+
for await (const entry of client.repositories.contents(repo.id, {
|
|
289
|
+
path: "src",
|
|
290
|
+
ref: "main",
|
|
291
|
+
})) {
|
|
292
|
+
console.log(entry.type, entry.path);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// One path: a directory page or a file, discriminated on `type`.
|
|
296
|
+
const readme = await client.repositories.contents.get(repo.id, "README.md");
|
|
297
|
+
if (readme.type === "file") console.log(readme.encoding, readme.content);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Enumerating a path that is a file throws a `ValidationError`; read it with
|
|
301
|
+
`contents.get()` instead.
|
|
302
|
+
|
|
303
|
+
History reads the same way: `commits()` pages back from a branch, tag or sha
|
|
304
|
+
(the default branch when omitted), and `commit()` returns one commit with its
|
|
305
|
+
changed files and unified diff.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
for await (const commit of client.repositories.commits(repo.id, {
|
|
309
|
+
sha: "main",
|
|
310
|
+
path: "src",
|
|
311
|
+
})) {
|
|
312
|
+
console.log(commit.sha, commit.message);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const detail = await client.repositories.commit(repo.id, "abc123");
|
|
316
|
+
console.log(
|
|
317
|
+
detail.files.map((file) => file.filename),
|
|
318
|
+
detail.patch,
|
|
319
|
+
);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`merge()` merges a branch or commit sha into a branch, like GitHub's merges API
|
|
323
|
+
(`repositories:write`). It resolves to the merge commit, or `null` when `base`
|
|
324
|
+
already contains `head`; a conflict throws a `ConflictError` and changes
|
|
325
|
+
nothing.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const merge = await client.repositories.merge(repo.id, {
|
|
329
|
+
base: "main",
|
|
330
|
+
head: "feature",
|
|
331
|
+
});
|
|
332
|
+
console.log(merge?.sha ?? "already up to date");
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Annotations
|
|
336
|
+
|
|
337
|
+
Annotations capture what a domain expert found good or bad on an OTel span. They
|
|
338
|
+
are labels, comments, and assignments—not numeric scores. Writes require an
|
|
339
|
+
authenticated business-member token with `annotations:write`; project API
|
|
340
|
+
keys and sandbox credentials cannot write annotations.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const client = new IntrospectionClient({
|
|
344
|
+
token: memberAccessToken,
|
|
345
|
+
cpSession: encodedMemberSession,
|
|
346
|
+
advanced: {
|
|
347
|
+
baseApiUrl: "https://api.introspection.dev", // Control Plane
|
|
348
|
+
dpUrl: memberDataPlaneUrl,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const span = {
|
|
353
|
+
trace_id: "0123456789abcdef0123456789abcdef",
|
|
354
|
+
span_id: "0123456789abcdef",
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
await client.projectLabels.create({
|
|
358
|
+
slug: "strong-structure",
|
|
359
|
+
color: "#f97316",
|
|
360
|
+
description: "A useful structure to preserve during distillation",
|
|
361
|
+
});
|
|
362
|
+
await client.annotations.create(span, { labels: ["strong-structure"] });
|
|
363
|
+
await client.annotations.create(span, {
|
|
364
|
+
comment: "Keep the conclusion before the evidence.",
|
|
365
|
+
});
|
|
366
|
+
await client.annotations.create(span, {
|
|
367
|
+
reviewerEmails: ["expert@example.com"],
|
|
368
|
+
});
|
|
369
|
+
await client.annotations.create(span, { reviewerEmails: [] });
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Labels and reviewers are complete snapshots; pass `[]` to clear one. Comments
|
|
373
|
+
append. Every mutation gets a UUIDv7 `event_id` before its
|
|
374
|
+
first transport attempt, so an automatic retry remains one event. Supply
|
|
375
|
+
`{ event_id }` as the final method argument when retrying across processes.
|
|
376
|
+
`client.annotations.list()` and `client.projectLabels.list()` are both awaitable
|
|
377
|
+
for the first page and async-iterable across every cursor page.
|
|
378
|
+
Read immutable annotation history with
|
|
379
|
+
`client.events.list({ event_name: "introspection.annotation", trace_id, span_id })`.
|
|
380
|
+
|
|
57
381
|
## Pi instrumentation
|
|
58
382
|
|
|
59
383
|
Pi is the supported agent-instrumentation path:
|
|
@@ -148,13 +472,14 @@ await logs.shutdown();
|
|
|
148
472
|
|
|
149
473
|
### Methods
|
|
150
474
|
|
|
151
|
-
| Method
|
|
152
|
-
|
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
475
|
+
| Method | Description |
|
|
476
|
+
| --------------------------------------- | -------------------------------------------------- |
|
|
477
|
+
| `logEvent(name, attributes?, options?)` | Log an app event under any custom name (see below) |
|
|
478
|
+
| `track(event, properties?)` | Track any user action (an alias of `logEvent`) |
|
|
479
|
+
| `feedback(type, options?)` | Track feedback on AI responses |
|
|
480
|
+
| `identify(userId, traits?)` | Associate a user with traits |
|
|
481
|
+
| `flush()` | Flush pending events |
|
|
482
|
+
| `shutdown()` | Shutdown and flush |
|
|
158
483
|
|
|
159
484
|
### Context helpers (OTel baggage)
|
|
160
485
|
|
|
@@ -166,6 +491,103 @@ await logs.shutdown();
|
|
|
166
491
|
| `withAnonymousId(id, callback)` | Set anonymous ID |
|
|
167
492
|
| `withBaggage(values, callback)` | Set arbitrary baggage values |
|
|
168
493
|
|
|
494
|
+
## Logging custom events
|
|
495
|
+
|
|
496
|
+
`logEvent(name, attributes?, options?)` records an app event under a name you
|
|
497
|
+
choose — `ark.feed.entry`, `checkout.completed` — so the platform stores it and
|
|
498
|
+
you can query it back by name. `track()` is a thin alias of it. Attributes are
|
|
499
|
+
stored under `properties.*`.
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
|
|
503
|
+
|
|
504
|
+
const logs = new IntrospectionLogs({ serviceName: "ark" }); // INTROSPECTION_TOKEN
|
|
505
|
+
|
|
506
|
+
logs.logEvent(
|
|
507
|
+
"ark.feed.entry",
|
|
508
|
+
{ entry_id: entry.id, source: "rss", score: 0.92 },
|
|
509
|
+
{
|
|
510
|
+
eventId: `feed-entry:${entry.id}`, // stable id: consumers dedupe on it
|
|
511
|
+
timestamp: entry.publishedAt, // Date or epoch ms; default now
|
|
512
|
+
identity: { userId: entry.ownerId }, // overrides the scoped identity
|
|
513
|
+
severity: "INFO", // DEBUG | INFO | WARN | ERROR
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
await logs.shutdown(); // flushes
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
After `init()`, the same call is available as a module-level function:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import * as introspection from "@introspection-sdk/introspection-node/otel";
|
|
524
|
+
|
|
525
|
+
await introspection.init({ serviceName: "ark" });
|
|
526
|
+
introspection.logEvent("ark.feed.entry", { entry_id: "e_1" });
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
- **Names.** Use your own namespace. Names starting with `introspection.`
|
|
530
|
+
(platform events) or `gen_ai.` (OpenTelemetry GenAI conventions) are
|
|
531
|
+
reserved and `logEvent` throws on them. `track()` delegates to `logEvent`,
|
|
532
|
+
so it rejects them too.
|
|
533
|
+
- **Idempotency.** Without `eventId` each call gets a fresh id. When delivery
|
|
534
|
+
can repeat (retries, replayed jobs, an agent re-running a step), pass an id
|
|
535
|
+
derived from the thing being recorded: re-sending the same `eventId` is how
|
|
536
|
+
consumers recognise and drop the duplicate.
|
|
537
|
+
- **Identity and context.** User / anonymous id and `gen_ai.conversation.id` /
|
|
538
|
+
agent come from the active baggage (`withUserId`, `withConversation`,
|
|
539
|
+
`withAgent`); `options.identity` overrides per field.
|
|
540
|
+
|
|
541
|
+
### From a recipe or agent sandbox
|
|
542
|
+
|
|
543
|
+
A Runtime sandbox is started with `INTROSPECTION_TOKEN` and
|
|
544
|
+
`INTROSPECTION_BASE_OTEL_URL` in its environment, so a recipe tool or
|
|
545
|
+
agent step needs no configuration — construct the logger and flush before the
|
|
546
|
+
step returns:
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
|
|
550
|
+
|
|
551
|
+
const events = new IntrospectionLogs({ serviceName: "ark-recipe" });
|
|
552
|
+
|
|
553
|
+
export async function recordFeedEntries(entries: FeedEntry[]) {
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
events.logEvent(
|
|
556
|
+
"ark.feed.entry",
|
|
557
|
+
{ entry_id: entry.id, title: entry.title, url: entry.url },
|
|
558
|
+
{ eventId: `ark.feed.entry:${entry.id}` },
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
await events.flush(); // the sandbox may be torn down after the step
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Reading custom events back
|
|
566
|
+
|
|
567
|
+
Custom events are served by `/v1/events` under the `introspection.track`
|
|
568
|
+
family, whose `payload` carries the original `name` and the `properties`:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { IntrospectionClient } from "@introspection-sdk/introspection-node";
|
|
572
|
+
|
|
573
|
+
const client = new IntrospectionClient();
|
|
574
|
+
|
|
575
|
+
type TrackPayload = { name: string; properties?: Record<string, unknown> };
|
|
576
|
+
|
|
577
|
+
for await (const ev of client.events.list({
|
|
578
|
+
event_name: "introspection.track",
|
|
579
|
+
lookback: "24h",
|
|
580
|
+
})) {
|
|
581
|
+
const payload = ev.payload as TrackPayload;
|
|
582
|
+
if (payload.name !== "ark.feed.entry") continue;
|
|
583
|
+
console.log(ev.timestamp, payload.properties?.entry_id);
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Filtering by `name` server-side is not available yet — it arrives with a
|
|
588
|
+
pending platform change. Until then, filter on `payload.name` client-side as
|
|
589
|
+
above.
|
|
590
|
+
|
|
169
591
|
## OpenTelemetry span processor
|
|
170
592
|
|
|
171
593
|
Attach the processor to a provider you already own, or stand one up with
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { OAuthToken } from "./auth.js";
|
|
2
|
+
import { DataPlaneClient } from "./data-plane.js";
|
|
3
|
+
import type { BearerCredentials } from "./http.js";
|
|
4
|
+
/** The `grant_type` of the native email-code grant. */
|
|
5
|
+
export declare const EMAIL_CODE_GRANT_TYPE = "urn:introspection:params:oauth:grant-type:email_code";
|
|
6
|
+
/**
|
|
7
|
+
* Where a session is kept between launches. The Web Storage shape (sync or
|
|
8
|
+
* async), so a file- or keychain-backed store or any `localStorage`-like
|
|
9
|
+
* object plugs in directly. Values are JSON strings.
|
|
10
|
+
*/
|
|
11
|
+
export interface SessionStorage {
|
|
12
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
13
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
14
|
+
removeItem(key: string): void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** Keeps the session in memory only; nothing survives a restart. */
|
|
17
|
+
export declare class InMemorySessionStorage implements SessionStorage {
|
|
18
|
+
private readonly values;
|
|
19
|
+
getItem(key: string): string | null;
|
|
20
|
+
setItem(key: string, value: string): void;
|
|
21
|
+
removeItem(key: string): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A signed-in member's session: the token response plus when it expires.
|
|
25
|
+
* Field names stay on the wire (snake_case), as the token response sends them.
|
|
26
|
+
*/
|
|
27
|
+
export interface AuthSession {
|
|
28
|
+
access_token: string;
|
|
29
|
+
token_type: string;
|
|
30
|
+
/** Rotated on every refresh. */
|
|
31
|
+
refresh_token: string | null;
|
|
32
|
+
/** Epoch milliseconds the access token expires at, when known. */
|
|
33
|
+
expires_at: number | null;
|
|
34
|
+
/** The granted scope: the Application's `allowed_scopes` ceiling. */
|
|
35
|
+
scope: string | null;
|
|
36
|
+
/** The platform session; refresh and sign-out are keyed on it. */
|
|
37
|
+
session_id: string | null;
|
|
38
|
+
org_id: string | null;
|
|
39
|
+
project_id: string | null;
|
|
40
|
+
/** The `customer` member the session belongs to. */
|
|
41
|
+
member_id: string | null;
|
|
42
|
+
/** The Data Plane URL for the session's project. */
|
|
43
|
+
dp_url: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** What changed, mirroring Supabase's `AuthChangeEvent`. */
|
|
46
|
+
export type AuthChangeEvent = "INITIAL_SESSION" | "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED";
|
|
47
|
+
export type AuthStateListener = (event: AuthChangeEvent, session: AuthSession | null) => void;
|
|
48
|
+
export interface AuthClientOptions {
|
|
49
|
+
/** The `native` Application's `client_id`. */
|
|
50
|
+
clientId: string;
|
|
51
|
+
/** The project the session is scoped to (slug or id). */
|
|
52
|
+
project: string;
|
|
53
|
+
/**
|
|
54
|
+
* CP API base URL. Defaults to `INTROSPECTION_BASE_API_URL` where a
|
|
55
|
+
* `process` global exists, then `https://api.introspection.dev`.
|
|
56
|
+
*/
|
|
57
|
+
baseApiUrl?: string;
|
|
58
|
+
/** Session persistence. Defaults to {@link InMemorySessionStorage}. */
|
|
59
|
+
storage?: SessionStorage;
|
|
60
|
+
/** Storage key; change it to keep separate sessions per environment. */
|
|
61
|
+
storageKey?: string;
|
|
62
|
+
/** Refresh this many seconds before expiry. Default `60`. */
|
|
63
|
+
leewaySeconds?: number;
|
|
64
|
+
/** Custom `fetch` (for tests or non-standard runtimes). */
|
|
65
|
+
fetch?: typeof fetch;
|
|
66
|
+
/** Clock in epoch milliseconds (for tests). */
|
|
67
|
+
now?: () => number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Email-code sign-in, session persistence and refresh for a `native`
|
|
71
|
+
* Application.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const auth = new AuthClient({
|
|
76
|
+
* clientId: "intro_app_…",
|
|
77
|
+
* project: "my-project",
|
|
78
|
+
* storage: sessionStore, // getItem / setItem / removeItem
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* await auth.signInWithOtp({ email });
|
|
82
|
+
* // A returning user's code is 6 digits; a new user's first code is 6
|
|
83
|
+
* // characters of A–Z and 0–9, so do not restrict the input to digits.
|
|
84
|
+
* await auth.verifyOtp({ email, token: code });
|
|
85
|
+
*
|
|
86
|
+
* const dp = await auth.dataPlane();
|
|
87
|
+
* const run = await dp.tasks.start({ prompt: "Hello", runtime_id });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare class AuthClient {
|
|
91
|
+
private readonly options;
|
|
92
|
+
private readonly baseApiUrl;
|
|
93
|
+
private readonly storage;
|
|
94
|
+
private readonly storageKey;
|
|
95
|
+
private readonly leewayMs;
|
|
96
|
+
private readonly fetchImpl;
|
|
97
|
+
private readonly now;
|
|
98
|
+
private current;
|
|
99
|
+
/** Bumped by every sign-in and sign-out; a response read under an older value is stale. */
|
|
100
|
+
private generation;
|
|
101
|
+
private restored;
|
|
102
|
+
private restoring?;
|
|
103
|
+
private refreshing?;
|
|
104
|
+
private storageWrite;
|
|
105
|
+
private readonly listeners;
|
|
106
|
+
constructor(options: AuthClientOptions);
|
|
107
|
+
/**
|
|
108
|
+
* Email a one-time code to `email`. The answer is the same whether or not
|
|
109
|
+
* the email has an account; a new address gets an account on verify.
|
|
110
|
+
* Throws `RateLimitError` (with `retryAfter`) when codes are requested too
|
|
111
|
+
* often.
|
|
112
|
+
*/
|
|
113
|
+
signInWithOtp(params: {
|
|
114
|
+
email: string;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Verify the code sent to `email` and sign in. The code is opaque: 6 digits
|
|
118
|
+
* for a returning user, 6 characters of A–Z and 0–9 for a new user's first
|
|
119
|
+
* sign-in. A wrong, expired or reused code throws a `ValidationError` with
|
|
120
|
+
* `code === "invalid_grant"`.
|
|
121
|
+
*
|
|
122
|
+
* Rejects with an `AbortError` when a sign-out or another sign-in completes
|
|
123
|
+
* while the code is being verified, so a slow response never overwrites a
|
|
124
|
+
* newer session.
|
|
125
|
+
*/
|
|
126
|
+
verifyOtp(params: {
|
|
127
|
+
email: string;
|
|
128
|
+
token: string;
|
|
129
|
+
}): Promise<AuthSession>;
|
|
130
|
+
/** Adopt a platform token obtained elsewhere, for example from your own backend. */
|
|
131
|
+
setSession(token: OAuthToken): Promise<AuthSession>;
|
|
132
|
+
/**
|
|
133
|
+
* The current session, restored from storage on first use and refreshed
|
|
134
|
+
* when it is within `leewaySeconds` of expiry. `null` when signed out.
|
|
135
|
+
*/
|
|
136
|
+
getSession(): Promise<AuthSession | null>;
|
|
137
|
+
/**
|
|
138
|
+
* Renew the session now, or join the renewal already in flight. A refresh
|
|
139
|
+
* the server rejects signs the user out and throws `AuthenticationError`;
|
|
140
|
+
* a network failure keeps the session and rethrows.
|
|
141
|
+
*/
|
|
142
|
+
refreshSession(): Promise<AuthSession>;
|
|
143
|
+
/**
|
|
144
|
+
* Sign out: forget the session locally, then revoke it on the platform.
|
|
145
|
+
* The local session is cleared even when revocation fails; the revocation
|
|
146
|
+
* error is still thrown.
|
|
147
|
+
*/
|
|
148
|
+
signOut(): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Listen for session changes. A new listener first receives
|
|
151
|
+
* `INITIAL_SESSION` with the session restored from storage (or `null`).
|
|
152
|
+
*/
|
|
153
|
+
onAuthStateChange(listener: AuthStateListener): {
|
|
154
|
+
unsubscribe(): void;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* A bearer credential backed by this client: refreshed before expiry, and
|
|
158
|
+
* once more after a `401` unless another request already renewed it.
|
|
159
|
+
*/
|
|
160
|
+
get credentials(): BearerCredentials;
|
|
161
|
+
/**
|
|
162
|
+
* A Data Plane client authenticated as the signed-in member. The Data Plane
|
|
163
|
+
* URL defaults to the one returned with the session.
|
|
164
|
+
*/
|
|
165
|
+
dataPlane(options?: {
|
|
166
|
+
dpUrl?: string;
|
|
167
|
+
additionalHeaders?: Record<string, string>;
|
|
168
|
+
}): Promise<DataPlaneClient>;
|
|
169
|
+
private postToken;
|
|
170
|
+
/** The refresh response omits `dp_url`, so it carries over from `previous`. */
|
|
171
|
+
private toSession;
|
|
172
|
+
private renew;
|
|
173
|
+
/** `expected` is the generation read before a sign-in request was sent. */
|
|
174
|
+
private signedIn;
|
|
175
|
+
/** Storage may be async, so writes are chained to land in transition order. */
|
|
176
|
+
private store;
|
|
177
|
+
private clear;
|
|
178
|
+
/** Concurrent first callers share one load, so none sees the session as absent while it is read. */
|
|
179
|
+
private restoreIfNeeded;
|
|
180
|
+
private emit;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=auth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../src/auth-client.ts"],"names":[],"mappings":"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"}
|