@openwop/openwop 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +64 -117
  2. package/dist/client.d.ts +131 -245
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +233 -440
  5. package/dist/client.js.map +1 -1
  6. package/dist/cost-attribution.d.ts +2 -2
  7. package/dist/cost-attribution.js +2 -2
  8. package/dist/envelope-directive.d.ts +1 -1
  9. package/dist/envelope-directive.js +1 -1
  10. package/dist/event-helpers.js +1 -1
  11. package/dist/event-helpers.js.map +1 -1
  12. package/dist/generated.d.ts +17 -0
  13. package/dist/generated.d.ts.map +1 -0
  14. package/dist/generated.js +311 -0
  15. package/dist/generated.js.map +1 -0
  16. package/dist/index.d.ts +16 -18
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +26 -56
  19. package/dist/index.js.map +1 -1
  20. package/dist/run-helpers.d.ts +20 -21
  21. package/dist/run-helpers.d.ts.map +1 -1
  22. package/dist/run-helpers.js +23 -72
  23. package/dist/run-helpers.js.map +1 -1
  24. package/dist/sse.d.ts +33 -15
  25. package/dist/sse.d.ts.map +1 -1
  26. package/dist/sse.js +28 -30
  27. package/dist/sse.js.map +1 -1
  28. package/dist/types.d.ts +253 -559
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/types.js.map +1 -1
  31. package/dist/webhook-header-families.d.ts +22 -13
  32. package/dist/webhook-header-families.d.ts.map +1 -1
  33. package/dist/webhook-header-families.js +30 -27
  34. package/dist/webhook-header-families.js.map +1 -1
  35. package/dist/webhook-helpers.browser.d.ts +15 -29
  36. package/dist/webhook-helpers.browser.d.ts.map +1 -1
  37. package/dist/webhook-helpers.browser.js +16 -31
  38. package/dist/webhook-helpers.browser.js.map +1 -1
  39. package/dist/webhook-helpers.d.ts +41 -41
  40. package/dist/webhook-helpers.d.ts.map +1 -1
  41. package/dist/webhook-helpers.js +40 -47
  42. package/dist/webhook-helpers.js.map +1 -1
  43. package/package.json +6 -4
  44. package/src/client.ts +255 -454
  45. package/src/cost-attribution.ts +2 -2
  46. package/src/envelope-directive.ts +1 -1
  47. package/src/event-helpers.ts +1 -1
  48. package/src/generated.ts +322 -0
  49. package/src/index.ts +78 -110
  50. package/src/run-helpers.ts +27 -85
  51. package/src/sse.ts +63 -42
  52. package/src/types.ts +268 -603
  53. package/src/webhook-header-families.ts +41 -29
  54. package/src/webhook-helpers.browser.ts +23 -32
  55. package/src/webhook-helpers.ts +67 -53
  56. package/dist/registry-helpers.d.ts +0 -118
  57. package/dist/registry-helpers.d.ts.map +0 -1
  58. package/dist/registry-helpers.js +0 -82
  59. package/dist/registry-helpers.js.map +0 -1
  60. package/src/registry-helpers.ts +0 -173
package/dist/client.js CHANGED
@@ -1,19 +1,35 @@
1
1
  /**
2
- * OpenwopClient — typed HTTP client for the openwop REST surface.
2
+ * OpenwopClient — typed HTTP client for the OpenWOP v2 REST surface.
3
3
  *
4
- * Hand-authored. Each method maps 1:1 to a documented endpoint in
5
- * ../../api/openapi.yaml. Request/response types live in ./types.ts.
4
+ * Hand-authored. Each method maps 1:1 to an operation in
5
+ * `spec/v2/path-manifest.json` (generated from `api/v2/openapi.yaml`):
6
+ * bare origin, unversioned path keys, negotiation by the `OpenWOP-Version`
7
+ * request header (RFC 0172 §A). Request/response types live in ./types.ts;
8
+ * the error-code union in ./generated.ts.
6
9
  *
7
- * Auth: a single bearer-style API key, supplied at construction. See
8
- * ../../auth.md for credential format.
10
+ * Auth: a single bearer-style API key, supplied at construction.
9
11
  */
10
- import { streamEvents } from './sse.js';
12
+ import { streamEvents, streamHostEvents } from './sse.js';
11
13
  import { WopError, } from './types.js';
14
+ /** The protocol major this SDK implements; the default for {@link OpenwopClientOptions.major}. */
15
+ export const SDK_PROTOCOL_MAJOR = 2;
16
+ /** Renders the `OpenWOP-Version` request value for a major: `<major>.0` (the OpenAPI grammar is `<major>.<minor>`). */
17
+ export function protocolVersionHeader(major) {
18
+ if (!Number.isInteger(major) || major < 0) {
19
+ throw new TypeError(`OpenwopClient: major must be a non-negative integer (got ${String(major)})`);
20
+ }
21
+ return `${major}.0`;
22
+ }
12
23
  export class OpenwopClient {
13
24
  #baseUrl;
14
25
  #apiKey;
15
26
  #fetch;
16
27
  #acceptLanguage;
28
+ #versionHeader;
29
+ /** The `OpenWOP-Version` value this client sends on every request. */
30
+ get protocolVersion() {
31
+ return this.#versionHeader;
32
+ }
17
33
  constructor(opts) {
18
34
  if (!opts.baseUrl)
19
35
  throw new TypeError('OpenwopClient: baseUrl is required');
@@ -23,121 +39,101 @@ export class OpenwopClient {
23
39
  this.#apiKey = opts.apiKey;
24
40
  this.#fetch = opts.fetch ?? fetch;
25
41
  this.#acceptLanguage = opts.acceptLanguage;
42
+ this.#versionHeader = protocolVersionHeader(opts.major ?? SDK_PROTOCOL_MAJOR);
26
43
  }
27
44
  // ── Discovery ────────────────────────────────────────────────────────
28
45
  discovery = {
46
+ /**
47
+ * `GET /.well-known/openwop` — one resource whose representation the
48
+ * `OpenWOP-Version` header selects (capabilities.md §1): with this
49
+ * client's major the host returns the closed v2 root.
50
+ */
29
51
  capabilities: () => this.#request({ method: 'GET', path: '/.well-known/openwop' }, false),
30
- openapi: () => this.#request({ method: 'GET', path: '/v1/openapi.json' }, false),
52
+ /** `GET /openapi.json` the self-describing OpenAPI 3.1 document. */
53
+ openapi: () => this.#request({ method: 'GET', path: '/openapi.json' }, false),
31
54
  };
32
55
  // ── Workflows ────────────────────────────────────────────────────────
33
56
  workflows = {
57
+ /** `GET /workflows/{workflowId}` */
34
58
  get: (workflowId) => this.#request({
35
59
  method: 'GET',
36
- path: `/v1/workflows/${encodeURIComponent(workflowId)}`,
60
+ path: `/workflows/${encodeURIComponent(workflowId)}`,
37
61
  }),
38
62
  };
39
63
  // ── Runs ─────────────────────────────────────────────────────────────
40
64
  runs = {
65
+ /** `POST /runs` — the body is closed at the composition (runs.md §Create). */
41
66
  create: (body, opts = {}) => this.#request({
42
67
  method: 'POST',
43
- path: '/v1/runs',
68
+ path: '/runs',
44
69
  body,
45
70
  headers: this.#mutationHeaders(opts),
46
71
  }),
72
+ /** `GET /runs/{runId}` — the snapshot (runs.md §Snapshot). */
47
73
  get: (runId) => this.#request({
48
74
  method: 'GET',
49
- path: `/v1/runs/${encodeURIComponent(runId)}`,
75
+ path: `/runs/${encodeURIComponent(runId)}`,
50
76
  }),
51
- /**
52
- * Fetch the portable JSON diagnostic export for a single run per
53
- * `spec/v1/debug-bundle.md`. The bundle's `redactionMode` reflects
54
- * the host's advertised `capabilities.compliance.defaultMode`; the
55
- * caller MUST treat masked/omitted/hashed fields as the
56
- * spec-canonical value. The `truncated` + `truncatedReason` fields
57
- * indicate the host hit its size cap.
58
- *
59
- * Returns `null` when the host doesn't advertise
60
- * `capabilities.debugBundle.supported: true` (the endpoint returns
61
- * 404 in that case per `debug-bundle.md` §"Authorization").
62
- */
63
- debugBundle: async (runId, opts = {}) => {
64
- const params = new URLSearchParams();
65
- if (opts.maxEvents !== undefined)
66
- params.set('maxEvents', String(opts.maxEvents));
67
- const query = params.toString();
68
- const path = `/v1/runs/${encodeURIComponent(runId)}/debug-bundle${query ? `?${query}` : ''}`;
69
- try {
70
- return await this.#request({ method: 'GET', path });
71
- }
72
- catch (err) {
73
- // Host doesn't advertise the capability → 404. Surface as null so callers
74
- // can branch on capability discovery without try/catch.
75
- if (err instanceof WopError && err.status === 404)
76
- return null;
77
- throw err;
78
- }
79
- },
77
+ /** `POST /runs/{runId}/cancel` — `status` is `cancelling` or `cancelled`. */
80
78
  cancel: (runId, body = {}, opts = {}) => this.#request({
81
79
  method: 'POST',
82
- path: `/v1/runs/${encodeURIComponent(runId)}/cancel`,
80
+ path: `/runs/${encodeURIComponent(runId)}/cancel`,
83
81
  body,
84
82
  headers: this.#mutationHeaders(opts),
85
83
  }),
84
+ /** `POST /runs/{runId}:pause` — `409` when the run is not pausable. */
86
85
  pause: (runId, body = {}, opts = {}) => this.#request({
87
86
  method: 'POST',
88
- path: `/v1/runs/${encodeURIComponent(runId)}:pause`,
87
+ path: `/runs/${encodeURIComponent(runId)}:pause`,
89
88
  body,
90
89
  headers: this.#mutationHeaders(opts),
91
90
  }),
91
+ /** `POST /runs/{runId}:resume` — `409` when the run is not paused. */
92
92
  resume: (runId, body = {}, opts = {}) => this.#request({
93
93
  method: 'POST',
94
- path: `/v1/runs/${encodeURIComponent(runId)}:resume`,
94
+ path: `/runs/${encodeURIComponent(runId)}:resume`,
95
95
  body,
96
96
  headers: this.#mutationHeaders(opts),
97
97
  }),
98
98
  /**
99
- * Bulk-cancel a set of in-flight runs in a single request per
100
- * `rest-endpoints.md` §"POST /v1/runs:bulk-cancel" (closes R1).
101
- * The top-level call returns 200 + per-id `results[]` whenever the
102
- * request reaches the host; partial failures surface inside the
103
- * array (each entry carries `ok: boolean` + optional `error`). Host-
104
- * defined cap on `runIds[]` length (RECOMMENDED 100); over-cap
105
- * returns `400 validation_error` with `details.maxRunIds`.
99
+ * `POST /runs:bulk-cancel` `200 { results[] }` in request order even
100
+ * when every id failed; per-id authorization yields `ok: false` with
101
+ * `run_forbidden` in that entry, never a top-level `403` (runs.md §Cancel).
106
102
  */
107
103
  bulkCancel: (body, opts = {}) => this.#request({
108
104
  method: 'POST',
109
- path: '/v1/runs:bulk-cancel',
105
+ path: '/runs:bulk-cancel',
110
106
  body,
111
107
  headers: this.#mutationHeaders(opts),
112
108
  }),
109
+ /** `POST /runs/{runId}:fork` — `mode: replay | branch` (runs.md §Fork; replay.md). */
113
110
  fork: (runId, body, opts = {}) => this.#request({
114
111
  method: 'POST',
115
- path: `/v1/runs/${encodeURIComponent(runId)}:fork`,
112
+ path: `/runs/${encodeURIComponent(runId)}:fork`,
116
113
  body,
117
114
  headers: this.#mutationHeaders(opts),
118
115
  }),
119
116
  /**
120
- * RFC 0056record a non-blocking quality annotation (rating / correction
121
- * / label / flag) on a run/event/node. Returns the persisted `Annotation`.
122
- * Throws on non-2xx (`501` when the host doesn't advertise
123
- * `capabilities.feedback.supported`).
117
+ * `POST /runs/{runId}/annotations` — a live notification, never a run
118
+ * event. Throws on non-2xx (`501` when the host doesn't advertise
119
+ * `feedback`).
124
120
  */
125
121
  createAnnotation: (runId, body, opts = {}) => this.#request({
126
122
  method: 'POST',
127
- path: `/v1/runs/${encodeURIComponent(runId)}/annotations`,
123
+ path: `/runs/${encodeURIComponent(runId)}/annotations`,
128
124
  body,
129
125
  headers: this.#mutationHeaders(opts),
130
126
  }),
131
127
  /**
132
- * RFC 0056list a run's annotations (tenant-scoped). Returns `null` when
133
- * the host doesn't advertise `capabilities.feedback` (404/501), so callers
134
- * can branch on capability discovery without try/catch.
128
+ * `GET /runs/{runId}/annotations`returns `null` when the host doesn't
129
+ * advertise `feedback` (404/501), so callers can branch on capability
130
+ * discovery without try/catch.
135
131
  */
136
132
  listAnnotations: async (runId) => {
137
133
  try {
138
134
  const res = await this.#request({
139
135
  method: 'GET',
140
- path: `/v1/runs/${encodeURIComponent(runId)}/annotations`,
136
+ path: `/runs/${encodeURIComponent(runId)}/annotations`,
141
137
  });
142
138
  return res.annotations;
143
139
  }
@@ -148,21 +144,17 @@ export class OpenwopClient {
148
144
  }
149
145
  },
150
146
  /**
151
- * RFC 0040 §C fetch the run's immediate parent in the cross-host
152
- * composition chain. Returns `parent: null` for top-level runs;
153
- * `parent.wellKnownUrl` is set when the parent is on a different
154
- * host, so callers walk the chain one hop at a time.
155
- *
147
+ * `GET /runs/{runId}/ancestry` — the run's immediate parent in the
148
+ * cross-host composition chain; `parent: null` for top-level runs.
156
149
  * Returns `null` when the host doesn't advertise
157
- * `capabilities.multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported: true`
158
- * (the endpoint returns 404 in that case per
159
- * `spec/v1/multi-agent-execution.md` §"GET /v1/runs/{runId}/ancestry").
150
+ * `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`
151
+ * (the endpoint 404s).
160
152
  */
161
153
  ancestry: async (runId) => {
162
154
  try {
163
155
  return await this.#request({
164
156
  method: 'GET',
165
- path: `/v1/runs/${encodeURIComponent(runId)}/ancestry`,
157
+ path: `/runs/${encodeURIComponent(runId)}/ancestry`,
166
158
  });
167
159
  }
168
160
  catch (err) {
@@ -172,18 +164,15 @@ export class OpenwopClient {
172
164
  }
173
165
  },
174
166
  /**
175
- * RFC 0054 — deterministic, replay-aware structured diff of two runs
176
- * (typically a run and its `:fork`). Requires `runs:read` on BOTH
177
- * `runId` and `against`. Returns `null` when the host doesn't
178
- * implement the endpoint (404 per `spec/v1/rest-endpoints.md`
179
- * §`GET /v1/runs/{runId}:diff`). `divergedAtSeq` is null + `eventDiffs`
180
- * empty when the two logs are identical.
167
+ * `GET /runs/{runId}:diff?against=` — deterministic, replay-aware diff of
168
+ * two runs; requires `runs:read` on both. OPTIONAL surface: `null` on
169
+ * `404`. Identical logs yield `divergedAtSeq: null` + empty `eventDiffs`.
181
170
  */
182
171
  diff: async (runId, against) => {
183
172
  try {
184
173
  return await this.#request({
185
174
  method: 'GET',
186
- path: `/v1/runs/${encodeURIComponent(runId)}:diff?against=${encodeURIComponent(against)}`,
175
+ path: `/runs/${encodeURIComponent(runId)}:diff?against=${encodeURIComponent(against)}`,
187
176
  });
188
177
  }
189
178
  catch (err) {
@@ -193,18 +182,15 @@ export class OpenwopClient {
193
182
  }
194
183
  },
195
184
  /**
196
- * RFC 0081 §C — the `EvalSummary` scorecard for a terminal eval run (one
197
- * started via `runs.create({ mode: 'eval', evalSuiteRef, agentId })`):
198
- * aggregate + per-task scores, cost, latency, schema-validity, and
199
- * redaction-safe safety findings. Returns `null` when the host doesn't
200
- * advertise `capabilities.agents.evalSuite` or the run isn't an eval run
201
- * (404). Throws `409` while the run is still in progress.
185
+ * `GET /runs/{runId}/eval-summary` — the `EvalSummary` for a terminal eval
186
+ * run. `null` on `404` (not an eval run, or `agents.evalSuite`
187
+ * unadvertised); throws `409` while the run is still in progress.
202
188
  */
203
189
  evalSummary: async (runId) => {
204
190
  try {
205
191
  return await this.#request({
206
192
  method: 'GET',
207
- path: `/v1/runs/${encodeURIComponent(runId)}/eval-summary`,
193
+ path: `/runs/${encodeURIComponent(runId)}/eval-summary`,
208
194
  });
209
195
  }
210
196
  catch (err) {
@@ -214,15 +200,14 @@ export class OpenwopClient {
214
200
  }
215
201
  },
216
202
  /**
217
- * Read a run-produced artifact by id (`GET /v1/runs/{runId}/artifacts/{artifactId}`).
218
- * The artifact body is implementation-defined per the host. Returns `null`
219
- * on 404 (no such artifact, or the host doesn't store artifacts).
203
+ * `GET /runs/{runId}/artifacts/{artifactId}` — an implementation-defined
204
+ * JSON object. `null` on `404`.
220
205
  */
221
206
  getArtifact: async (runId, artifactId) => {
222
207
  try {
223
208
  return await this.#request({
224
209
  method: 'GET',
225
- path: `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`,
210
+ path: `/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}`,
226
211
  });
227
212
  }
228
213
  catch (err) {
@@ -231,10 +216,51 @@ export class OpenwopClient {
231
216
  throw err;
232
217
  }
233
218
  },
219
+ /**
220
+ * `GET /runs/{runId}/compensation` (RFC 0173 §C.1) — the compensation
221
+ * plan and attempts. Gated on `compensation`; `null` on `404`.
222
+ */
223
+ compensation: async (runId) => {
224
+ try {
225
+ return await this.#request({
226
+ method: 'GET',
227
+ path: `/runs/${encodeURIComponent(runId)}/compensation`,
228
+ });
229
+ }
230
+ catch (err) {
231
+ if (err instanceof WopError && err.status === 404)
232
+ return null;
233
+ throw err;
234
+ }
235
+ },
236
+ /**
237
+ * `GET /runs/{runId}/effects` (RFC 0173 §C.2) — the Layer-2 effect ledger,
238
+ * business-identity keyed. Gated on `idempotency`; `null` on `404`.
239
+ */
240
+ effects: async (runId) => {
241
+ try {
242
+ return await this.#request({
243
+ method: 'GET',
244
+ path: `/runs/${encodeURIComponent(runId)}/effects`,
245
+ });
246
+ }
247
+ catch (err) {
248
+ if (err instanceof WopError && err.status === 404)
249
+ return null;
250
+ throw err;
251
+ }
252
+ },
253
+ /**
254
+ * `GET /runs/{runId}/events/poll` — the long-poll fallback (events.md
255
+ * §Poll). `afterSequence` returns events with `sequence > afterSequence`;
256
+ * omission means from the first event. The response's `lastSequence` is
257
+ * the highest sequence in the log (`-1` when empty) — feed it back as the
258
+ * next `afterSequence`.
259
+ */
234
260
  pollEvents: (runId, params = {}) => {
235
261
  const search = new URLSearchParams();
236
- if (params.lastSequence !== undefined) {
237
- search.set('lastSequence', String(params.lastSequence));
262
+ if (params.afterSequence !== undefined) {
263
+ search.set('afterSequence', String(params.afterSequence));
238
264
  }
239
265
  if (params.timeoutSeconds !== undefined) {
240
266
  search.set('timeout', String(params.timeoutSeconds));
@@ -242,25 +268,38 @@ export class OpenwopClient {
242
268
  const qs = search.toString();
243
269
  return this.#request({
244
270
  method: 'GET',
245
- path: `/v1/runs/${encodeURIComponent(runId)}/events/poll${qs ? `?${qs}` : ''}`,
271
+ path: `/runs/${encodeURIComponent(runId)}/events/poll${qs ? `?${qs}` : ''}`,
246
272
  });
247
273
  },
248
274
  /**
249
- * Async-iterable SSE consumer. The connection auto-closes when the
250
- * server closes the stream (terminal run event); break out of the
251
- * loop or call `signal.abort()` to terminate early.
275
+ * `GET /runs/{runId}/events` — async-iterable SSE consumer. The
276
+ * connection auto-closes after the run's terminal event; break out of
277
+ * the loop or call `signal.abort()` to terminate early.
278
+ */
279
+ events: (runId, opts = {}) => streamEvents(this.#streamContext(), runId, opts),
280
+ };
281
+ // ── Host ─────────────────────────────────────────────────────────────
282
+ host = {
283
+ /**
284
+ * `GET /host/effect-seams` (RFC 0173 §C) — every outbound effect seam
285
+ * replay suppression covers. Throws `401` when unauthenticated.
252
286
  */
253
- events: (runId, opts = {}) => streamEvents({ baseUrl: this.#baseUrl, apiKey: this.#apiKey }, runId, opts),
287
+ effectSeams: () => this.#request({ method: 'GET', path: '/host/effect-seams' }),
288
+ /**
289
+ * `GET /host/events` — the `hostEvents` channel (heartbeat messages) as
290
+ * SSE; content-free of run data. A host MAY declare another address
291
+ * under `heartbeat.deliveryChannel` — pass it as `opts.path`.
292
+ */
293
+ events: (opts = {}) => streamHostEvents(this.#streamContext(), opts),
254
294
  };
255
295
  // ── Manifest-agent inventory (RFC 0072 §A) ───────────────────────────
256
- // Read-only. Gated on `capabilities.agents.manifestRuntime`; both methods
257
- // return `null` when the host doesn't advertise it (the endpoints 404).
258
- // Dispatch is not here: a manifest agent runs as a `runs.create` whose
259
- // workflow node pins it via `WorkflowNode.agent` (RFC 0072 §B).
296
+ // Read-only. Gated on `agents`; the methods return `null` when the host
297
+ // doesn't advertise it (the endpoints 404).
260
298
  agents = {
299
+ /** `GET /agents` */
261
300
  list: async () => {
262
301
  try {
263
- return await this.#request({ method: 'GET', path: '/v1/agents' });
302
+ return await this.#request({ method: 'GET', path: '/agents' });
264
303
  }
265
304
  catch (err) {
266
305
  if (err instanceof WopError && err.status === 404)
@@ -268,11 +307,12 @@ export class OpenwopClient {
268
307
  throw err;
269
308
  }
270
309
  },
310
+ /** `GET /agents/{agentId}` */
271
311
  get: async (agentId) => {
272
312
  try {
273
313
  return await this.#request({
274
314
  method: 'GET',
275
- path: `/v1/agents/${encodeURIComponent(agentId)}`,
315
+ path: `/agents/${encodeURIComponent(agentId)}`,
276
316
  });
277
317
  }
278
318
  catch (err) {
@@ -281,17 +321,12 @@ export class OpenwopClient {
281
321
  throw err;
282
322
  }
283
323
  },
284
- /**
285
- * RFC 0082 §C/§E — list a manifest agent's deployment records (per-(agentId,
286
- * version) lifecycle state + channels + canary + rollback pointer). Returns
287
- * `null` when the host doesn't advertise `capabilities.agents.deployment`
288
- * (the endpoint 404s).
289
- */
324
+ /** `GET /agents/{agentId}/deployments` (RFC 0082 §C/§E) — `null` when unadvertised. */
290
325
  listDeployments: async (agentId) => {
291
326
  try {
292
327
  return await this.#request({
293
328
  method: 'GET',
294
- path: `/v1/agents/${encodeURIComponent(agentId)}/deployments`,
329
+ path: `/agents/${encodeURIComponent(agentId)}/deployments`,
295
330
  });
296
331
  }
297
332
  catch (err) {
@@ -300,28 +335,17 @@ export class OpenwopClient {
300
335
  throw err;
301
336
  }
302
337
  },
303
- /**
304
- * RFC 0082 §E — request a deployment state transition (promote / pause /
305
- * deprecate / rollback / adjust-canary). The host authorizes fail-closed
306
- * against the RFC 0049 `deploy:*` scope, runs any RFC 0051 approvalGate, and
307
- * enforces RFC 0081 `requiredEval` before emitting `deployment.promoted`.
308
- * Returns the updated deployment record. Throws on non-2xx (`403` fail-closed
309
- * / `eval_gate_unmet`; `400` `no_active_deployment` / unsupported state).
310
- */
338
+ /** `POST /agents/{agentId}/deployments` (RFC 0082 §E) — a deployment state transition. */
311
339
  transitionDeployment: (agentId, body, opts = {}) => this.#request({
312
340
  method: 'POST',
313
- path: `/v1/agents/${encodeURIComponent(agentId)}/deployments`,
341
+ path: `/agents/${encodeURIComponent(agentId)}/deployments`,
314
342
  body,
315
343
  headers: this.#mutationHeaders(opts),
316
344
  }),
317
- /**
318
- * RFC 0086 §B — list the standing agent roster (named instances + their
319
- * workflow portfolios) visible to the caller. Returns `null` when the host
320
- * doesn't advertise `capabilities.agents.roster` (the endpoint 404s).
321
- */
345
+ /** `GET /agents/roster` (RFC 0086 §B) — `null` when unadvertised. */
322
346
  listRoster: async () => {
323
347
  try {
324
- return await this.#request({ method: 'GET', path: '/v1/agents/roster' });
348
+ return await this.#request({ method: 'GET', path: '/agents/roster' });
325
349
  }
326
350
  catch (err) {
327
351
  if (err instanceof WopError && err.status === 404)
@@ -329,15 +353,12 @@ export class OpenwopClient {
329
353
  throw err;
330
354
  }
331
355
  },
332
- /**
333
- * RFC 0086 §B — return one standing roster entry. Returns `null` on 404
334
- * (no such entry, cross-tenant, or the capability is unadvertised).
335
- */
356
+ /** `GET /agents/roster/{rosterId}` (RFC 0086 §B) — `null` on `404`. */
336
357
  getRosterEntry: async (rosterId) => {
337
358
  try {
338
359
  return await this.#request({
339
360
  method: 'GET',
340
- path: `/v1/agents/roster/${encodeURIComponent(rosterId)}`,
361
+ path: `/agents/roster/${encodeURIComponent(rosterId)}`,
341
362
  });
342
363
  }
343
364
  catch (err) {
@@ -346,14 +367,10 @@ export class OpenwopClient {
346
367
  throw err;
347
368
  }
348
369
  },
349
- /**
350
- * RFC 0087 §C — return the caller's agent org-chart (departments + roles +
351
- * `reportsTo` over roster members; descriptive — confers no authority).
352
- * Returns `null` when the host doesn't advertise `capabilities.agents.orgChart`.
353
- */
370
+ /** `GET /agents/org-chart` (RFC 0087 §C) — `null` when unadvertised. */
354
371
  getOrgChart: async () => {
355
372
  try {
356
- return await this.#request({ method: 'GET', path: '/v1/agents/org-chart' });
373
+ return await this.#request({ method: 'GET', path: '/agents/org-chart' });
357
374
  }
358
375
  catch (err) {
359
376
  if (err instanceof WopError && err.status === 404)
@@ -361,18 +378,13 @@ export class OpenwopClient {
361
378
  throw err;
362
379
  }
363
380
  },
364
- /**
365
- * RFC 0087 §D — one department's subtree + responsibility roll-up (the
366
- * union of its members' RFC 0086 portfolios). `recursive: false` scopes the
367
- * roll-up to direct members. Returns `null` on 404 (unknown/cross-tenant
368
- * department, or the capability is unadvertised).
369
- */
381
+ /** `GET /agents/org-chart/{departmentId}` (RFC 0087 §D) — `null` on `404`. */
370
382
  getOrgChartDepartment: async (departmentId, opts = {}) => {
371
383
  const qs = opts.recursive === false ? '?recursive=false' : '';
372
384
  try {
373
385
  return await this.#request({
374
386
  method: 'GET',
375
- path: `/v1/agents/org-chart/${encodeURIComponent(departmentId)}${qs}`,
387
+ path: `/agents/org-chart/${encodeURIComponent(departmentId)}${qs}`,
376
388
  });
377
389
  }
378
390
  catch (err) {
@@ -382,18 +394,12 @@ export class OpenwopClient {
382
394
  }
383
395
  },
384
396
  };
385
- // RFC 0078 — portable tool catalog (spec/v1/tool-catalog.md). The host
386
- // projects every node-pack / workflow / mcp / connector / host-extension
387
- // tool visible to the caller onto a uniform `ToolDescriptor`.
397
+ // ── RFC 0078 — portable tool catalog (gated on `toolCatalog`) ─────────
388
398
  tools = {
389
- /**
390
- * RFC 0078 §B — list the portable `ToolDescriptor`s visible to the caller.
391
- * Returns `null` when the host doesn't advertise `capabilities.toolCatalog`
392
- * (the endpoint 404s), so callers can branch on capability discovery.
393
- */
399
+ /** `GET /tools` — `null` when the host doesn't advertise `toolCatalog`. */
394
400
  list: async () => {
395
401
  try {
396
- return await this.#request({ method: 'GET', path: '/v1/tools' });
402
+ return await this.#request({ method: 'GET', path: '/tools' });
397
403
  }
398
404
  catch (err) {
399
405
  if (err instanceof WopError && err.status === 404)
@@ -401,17 +407,12 @@ export class OpenwopClient {
401
407
  throw err;
402
408
  }
403
409
  },
404
- /**
405
- * RFC 0112 — list the `CompactToolDescriptor`s via `GET /v1/tools?view=compact`
406
- * when the host advertises `capabilities.toolCatalog.compactView`. Unwraps the
407
- * `{ tools: CompactToolDescriptor[] }` envelope. Returns `null` when the host
408
- * doesn't advertise the catalog (the endpoint 404s).
409
- */
410
+ /** `GET /tools?view=compact` (RFC 0112) — unwraps `{ tools }`; `null` when unadvertised. */
410
411
  listCompact: async () => {
411
412
  try {
412
413
  const res = await this.#request({
413
414
  method: 'GET',
414
- path: '/v1/tools?view=compact',
415
+ path: '/tools?view=compact',
415
416
  });
416
417
  return res.tools ?? [];
417
418
  }
@@ -421,18 +422,13 @@ export class OpenwopClient {
421
422
  throw err;
422
423
  }
423
424
  },
424
- /**
425
- * RFC 0078 §B — return one `ToolDescriptor` by its stable `toolId`. Returns
426
- * `null` on 404 (no such tool, or the capability is unadvertised). Pass
427
- * `{ view: 'compact' }` (RFC 0112) to receive the `CompactToolDescriptor`
428
- * projection instead.
429
- */
425
+ /** `GET /tools/{toolId}` — `null` on `404`; `{ view: 'compact' }` for the compact projection. */
430
426
  get: async (toolId, opts = {}) => {
431
427
  const query = opts.view === 'compact' ? '?view=compact' : '';
432
428
  try {
433
429
  return await this.#request({
434
430
  method: 'GET',
435
- path: `/v1/tools/${encodeURIComponent(toolId)}${query}`,
431
+ path: `/tools/${encodeURIComponent(toolId)}${query}`,
436
432
  });
437
433
  }
438
434
  catch (err) {
@@ -442,136 +438,57 @@ export class OpenwopClient {
442
438
  }
443
439
  },
444
440
  };
445
- // ── User-authored agents (sample-extension; non-normative) ───────────
446
- // Backs the workflow-engine sample app's Agents tab. Pack-installed
447
- // agents come through the `.agents` inventory above (RFC 0072 §A).
448
- // These methods wrap the `POST/DELETE /v1/host/sample/agents` +
449
- // `GET/POST /v1/host/sample/registry/agent-packs` host extensions.
450
- // Returns `null` on 404 (capability absent — matches the `.agents`
451
- // surface pattern); throws on other failures.
452
- userAgents = {
453
- create: async (body, opts = {}) => {
454
- return await this.#request({
455
- method: 'POST',
456
- path: '/v1/host/sample/agents',
457
- body,
458
- ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
459
- });
460
- },
461
- delete: async (agentId) => {
462
- try {
463
- await this.#request({
464
- method: 'DELETE',
465
- path: `/v1/host/sample/agents/${encodeURIComponent(agentId)}`,
466
- });
467
- return true;
468
- }
469
- catch (err) {
470
- if (err instanceof WopError && err.status === 404)
471
- return false;
472
- throw err;
473
- }
474
- },
475
- listAvailablePacks: async () => {
476
- try {
477
- return await this.#request({
478
- method: 'GET',
479
- path: '/v1/host/sample/registry/agent-packs',
480
- });
481
- }
482
- catch (err) {
483
- if (err instanceof WopError && err.status === 404)
484
- return null;
485
- throw err;
486
- }
487
- },
488
- installPack: async (body) => {
489
- return await this.#request({
490
- method: 'POST',
491
- path: '/v1/host/sample/registry/agent-packs/install',
492
- body,
493
- });
494
- },
495
- };
496
441
  // ── HITL interrupts (run-scoped + signed-token) ──────────────────────
497
442
  interrupts = {
443
+ /** `POST /runs/{runId}/interrupts/{nodeId}` */
498
444
  resolveByRun: (runId, nodeId, body, opts = {}) => this.#request({
499
445
  method: 'POST',
500
- path: `/v1/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(nodeId)}`,
446
+ path: `/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(nodeId)}`,
501
447
  body,
502
448
  headers: this.#mutationHeaders(opts),
503
449
  }),
504
450
  /**
505
- * Inspect an interrupt via signed token useful for showing the
506
- * interrupt's `kind`, `data`, and `resumeSchema` to a downstream
507
- * UI before the user resolves. Token is the auth, no API key
508
- * required (signed-token endpoints intentionally bypass bearer
509
- * auth so external systems can resolve without openwop credentials).
451
+ * `GET /interrupts/{token}` inspect via signed token. The token is the
452
+ * auth; no API key is sent (signed-token endpoints bypass bearer auth so
453
+ * external systems can resolve without openwop credentials).
510
454
  */
511
455
  inspectByToken: (token) => this.#request({
512
456
  method: 'GET',
513
- path: `/v1/interrupts/${encodeURIComponent(token)}`,
457
+ path: `/interrupts/${encodeURIComponent(token)}`,
514
458
  }, false),
515
- /**
516
- * Resolve an interrupt via signed token — used by external
517
- * systems (calendar webhooks, payment confirmations) that the
518
- * engine handed a callback URL at suspension time.
519
- */
459
+ /** `POST /interrupts/{token}` — resolve via signed token. */
520
460
  resolveByToken: (token, body, opts = {}) => this.#request({
521
461
  method: 'POST',
522
- path: `/v1/interrupts/${encodeURIComponent(token)}`,
462
+ path: `/interrupts/${encodeURIComponent(token)}`,
523
463
  body,
524
464
  headers: this.#mutationHeaders(opts),
525
465
  }, false),
526
466
  };
527
- // ── Webhook subscriptions (per spec/v1/webhooks.md) ─────────────────────
467
+ // ── Webhook subscriptions (webhooks.md; gated on `webhooks`) ─────────
528
468
  webhooks = {
529
469
  /**
530
- * Register a webhook subscription. Server signs deliveries with
531
- * HMAC-SHA256 over `${timestamp}.${rawBody}` using the
532
- * registration-time secret per `spec/v1/webhooks.md` §"Signature
533
- * recipe". The secret is returned ONCE in the response — store it
534
- * server-side for verification; the host cannot recover it.
470
+ * `POST /webhooks` `{ url, events[], secret?, tags? }`; `url` MUST be
471
+ * `https://`. Deliveries are signed HMAC-SHA256 over
472
+ * `${timestamp}.${rawBody}` under the `OpenWOP-*` header family (verify
473
+ * with `@openwop/openwop/webhooks`).
535
474
  */
536
475
  register: (body, opts = {}) => this.#request({
537
476
  method: 'POST',
538
- path: '/v1/webhooks',
477
+ path: '/webhooks',
539
478
  body,
540
479
  headers: this.#mutationHeaders(opts),
541
480
  }),
542
- /**
543
- * Unregister a webhook subscription. Returns void on success;
544
- * throws `WopError` with `subscription_not_found` on unknown
545
- * subscriptionId.
546
- */
547
- unregister: async (subscriptionId) => {
481
+ /** `DELETE /webhooks/{webhookId}` — `204`; throws `404` when unknown, `403` outside the tenant. */
482
+ unregister: async (webhookId) => {
548
483
  await this.#request({
549
484
  method: 'DELETE',
550
- path: `/v1/webhooks/${encodeURIComponent(subscriptionId)}`,
485
+ path: `/webhooks/${encodeURIComponent(webhookId)}`,
551
486
  });
552
487
  },
553
488
  };
554
- // ── Prompt library (RFC 0028; gated on capabilities.prompts.*) ──
555
- //
556
- // Read endpoints (list, get, render) gate on
557
- // `capabilities.prompts.endpointsSupported: true`. Mutating endpoints
558
- // (create, update, delete) additionally require
559
- // `capabilities.prompts.mutableLibrary: true`. Hosts that don't advertise
560
- // the relevant capability return `501 capability_not_provided`; the SDK
561
- // surfaces that as a `WopError`. Clients SHOULD pre-flight via
562
- // `getCapabilities()` before calling.
563
- //
564
- // NOTE: `capabilities.prompts.supported: true` (without
565
- // `endpointsSupported: true`) ONLY gates node-execution PromptRef
566
- // resolution per RFC 0027 Phase A; it does NOT imply these endpoints are
567
- // available. See spec/v1/prompts.md §"Capability advertisement" for the
568
- // two-axis gating split.
489
+ // ── Prompt library (RFC 0028; gated on `prompts`) ────────────────────
569
490
  prompts = {
570
- /**
571
- * List prompt templates available to the caller per RFC 0028 §A
572
- * (operationId `listPromptTemplates`). Supports kind / tag / modelClass
573
- * / source filters + opaque cursor pagination.
574
- */
491
+ /** `GET /prompts` — kind / tag / modelClass / source filters + cursor pagination. */
575
492
  list: (req = {}) => {
576
493
  const search = new URLSearchParams();
577
494
  if (req.kind)
@@ -589,15 +506,10 @@ export class OpenwopClient {
589
506
  const query = search.toString();
590
507
  return this.#request({
591
508
  method: 'GET',
592
- path: `/v1/prompts${query ? `?${query}` : ''}`,
509
+ path: `/prompts${query ? `?${query}` : ''}`,
593
510
  });
594
511
  },
595
- /**
596
- * Fetch a single PromptTemplate by id per RFC 0028 §A
597
- * (operationId `getPromptTemplate`). Optionally pin a SemVer
598
- * `version`; supply `libraryId` to disambiguate when multiple installed
599
- * packs ship the same templateId.
600
- */
512
+ /** `GET /prompts/{templateId}` — `null` on `404`; a `400 prompt_ref_ambiguous` still throws. */
601
513
  get: async (req) => {
602
514
  const search = new URLSearchParams();
603
515
  if (req.version)
@@ -608,90 +520,55 @@ export class OpenwopClient {
608
520
  try {
609
521
  return await this.#request({
610
522
  method: 'GET',
611
- path: `/v1/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
523
+ path: `/prompts/${encodeURIComponent(req.templateId)}${query ? `?${query}` : ''}`,
612
524
  });
613
525
  }
614
526
  catch (err) {
615
- // Return null on 404 (no such template), consistent with the other
616
- // get-by-id methods (`agents.get`, `tools.get`, …) and the Python/Go
617
- // SDKs; a `400 prompt_ref_ambiguous` and other errors still throw so
618
- // callers can distinguish "not found" from "ambiguous reference".
619
527
  if (err instanceof WopError && err.status === 404)
620
528
  return null;
621
529
  throw err;
622
530
  }
623
531
  },
624
- /**
625
- * Render a PromptTemplate with supplied variable bindings per RFC 0028
626
- * §A (operationId `renderPromptTemplate`). Returns composed body +
627
- * sha256 hash + per-variable hashes. The deterministic-hash invariant
628
- * (RFC 0028 §A) requires the `hash` to match what a matching
629
- * `prompt.composed` event would carry at dispatch time. Does NOT
630
- * dispatch an LLM call. Secret-source variable values MUST be supplied
631
- * as `[REDACTED:<credentialRef>]` markers per
632
- * SECURITY/threat-model-secret-leakage.md §SR-1.
633
- */
532
+ /** `POST /prompts:render` — composed body + sha256 hash; does NOT dispatch an LLM call. */
634
533
  render: (req) => {
635
534
  return this.#request({
636
535
  method: 'POST',
637
- path: '/v1/prompts:render',
536
+ path: '/prompts:render',
638
537
  body: req,
639
538
  });
640
539
  },
641
- /**
642
- * Create a new user-source PromptTemplate per RFC 0028 §A
643
- * (operationId `createPromptTemplate`). Mutating endpoint —
644
- * requires `capabilities.prompts.mutableLibrary: true`. Supports
645
- * `Idempotency-Key` per the standard `MutationOptions` pattern.
646
- */
540
+ /** `POST /prompts` — requires `prompts.mutableLibrary`. */
647
541
  create: (template, opts = {}) => {
648
542
  return this.#request({
649
543
  method: 'POST',
650
- path: '/v1/prompts',
544
+ path: '/prompts',
651
545
  body: template,
652
546
  headers: this.#mutationHeaders(opts),
653
547
  });
654
548
  },
655
- /**
656
- * Replace an existing user-source PromptTemplate per RFC 0028 §A
657
- * (operationId `updatePromptTemplate`). Submitted SemVer MUST be
658
- * strictly greater than stored. Mutating endpoint — requires
659
- * `capabilities.prompts.mutableLibrary: true`. Pack-sourced and
660
- * host-built-in templates are read-only (host returns 403).
661
- */
549
+ /** `PUT /prompts/{templateId}` — the submitted SemVer MUST be strictly greater than stored. */
662
550
  update: (templateId, template, opts = {}) => {
663
551
  return this.#request({
664
552
  method: 'PUT',
665
- path: `/v1/prompts/${encodeURIComponent(templateId)}`,
553
+ path: `/prompts/${encodeURIComponent(templateId)}`,
666
554
  body: template,
667
555
  headers: this.#mutationHeaders(opts),
668
556
  });
669
557
  },
670
- /**
671
- * Delete a user-source PromptTemplate per RFC 0028 §A
672
- * (operationId `deletePromptTemplate`). Mutating endpoint —
673
- * requires `capabilities.prompts.mutableLibrary: true`. Pack-sourced
674
- * and host-built-in templates are read-only (host returns 403).
675
- */
558
+ /** `DELETE /prompts/{templateId}` */
676
559
  delete: (templateId) => {
677
560
  return this.#request({
678
561
  method: 'DELETE',
679
- path: `/v1/prompts/${encodeURIComponent(templateId)}`,
562
+ path: `/prompts/${encodeURIComponent(templateId)}`,
680
563
  });
681
564
  },
682
565
  };
683
- // ── Audit-log integrity (gated on openwop-audit-log-integrity profile) ──
566
+ // ── Audit-log integrity ──────────────────────────────────────────────
684
567
  audit = {
685
568
  /**
686
- * Verify the audit-log hash chain over `[fromSeq, toSeq]` per
687
- * `auth-profiles.md` §`openwop-audit-log-integrity` §4. Requires
688
- * the `audit:read` scope on the API key. Returns chain-validity
689
- * verdict + signed checkpoints + any detected anomalies.
690
- *
691
- * Hosts that do NOT advertise the profile return `404` and throw
692
- * a `WopError`. Clients SHOULD pre-flight via
693
- * `client.getCapabilities()` (or directly inspect
694
- * `capabilities.auth.profiles[]`) before calling.
569
+ * `GET /audit/verify?fromSeq&toSeq` chain-validity verdict + signed
570
+ * checkpoints + anomalies. Requires the `audit:read` scope; a host that
571
+ * does not serve the profile answers `404`.
695
572
  */
696
573
  verify: (fromSeq, toSeq) => {
697
574
  const search = new URLSearchParams();
@@ -699,19 +576,18 @@ export class OpenwopClient {
699
576
  search.set('toSeq', String(toSeq));
700
577
  return this.#request({
701
578
  method: 'GET',
702
- path: `/v1/audit/verify?${search.toString()}`,
579
+ path: `/audit/verify?${search.toString()}`,
703
580
  });
704
581
  },
705
582
  };
706
- // ── RFC 0103 Localized content surface (gated on capabilities.content) ──
583
+ // ── RFC 0103 Localized content surface (gated on `content`) ──────────
707
584
  content = {
708
- /** `GET /v1/content/pages` — list page records. Returns `null` when the
709
- * host doesn't advertise `capabilities.content` (501). */
585
+ /** `GET /content/pages` — `null` when the host doesn't advertise `content` (501). */
710
586
  listPages: async () => {
711
587
  try {
712
588
  return await this.#request({
713
589
  method: 'GET',
714
- path: '/v1/content/pages',
590
+ path: '/content/pages',
715
591
  });
716
592
  }
717
593
  catch (err) {
@@ -720,15 +596,12 @@ export class OpenwopClient {
720
596
  throw err;
721
597
  }
722
598
  },
723
- /** `GET /v1/content/pages/{slug}` — the negotiated locale's resolved page +
724
- * sections. `acceptLanguage` rides the `Accept-Language` header (the Stable
725
- * `i18n.md` negotiation; no `?locale=`). Returns `null` on `404`
726
- * (no such published page) or `501` (uncapable). */
599
+ /** `GET /content/pages/{slug}` — `acceptLanguage` rides `Accept-Language`; `null` on `404`/`501`. */
727
600
  getPage: async (slug, acceptLanguage) => {
728
601
  try {
729
602
  return await this.#request({
730
603
  method: 'GET',
731
- path: `/v1/content/pages/${encodeURIComponent(slug)}`,
604
+ path: `/content/pages/${encodeURIComponent(slug)}`,
732
605
  ...(acceptLanguage
733
606
  ? { headers: { 'Accept-Language': acceptLanguage } }
734
607
  : {}),
@@ -740,27 +613,24 @@ export class OpenwopClient {
740
613
  throw err;
741
614
  }
742
615
  },
743
- /** `POST /v1/content/pages` — create a page record (admin). Throws the
744
- * typed `WopError` on `400`/`401`/`403`. */
616
+ /** `POST /content/pages` (admin). */
745
617
  createPage: (body) => this.#request({
746
618
  method: 'POST',
747
- path: '/v1/content/pages',
619
+ path: '/content/pages',
748
620
  body,
749
621
  }),
750
- /** `PUT /v1/content/pages/{pageId}/sections/{sectionId}` upsert a
751
- * section's field overlay for a locale (admin). */
622
+ /** `PUT /content/pages/{pageId}/sections/{sectionId}` (admin). */
752
623
  putSection: (pageId, sectionId, body) => this.#request({
753
624
  method: 'PUT',
754
- path: `/v1/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
625
+ path: `/content/pages/${encodeURIComponent(pageId)}/sections/${encodeURIComponent(sectionId)}`,
755
626
  body,
756
627
  }),
757
- /** `GET /v1/content/settings` — language settings. Returns `null` when the
758
- * host doesn't advertise `capabilities.content` (501). */
628
+ /** `GET /content/settings` — `null` when the host doesn't advertise `content` (501). */
759
629
  getSettings: async () => {
760
630
  try {
761
631
  return await this.#request({
762
632
  method: 'GET',
763
- path: '/v1/content/settings',
633
+ path: '/content/settings',
764
634
  });
765
635
  }
766
636
  catch (err) {
@@ -769,127 +639,45 @@ export class OpenwopClient {
769
639
  throw err;
770
640
  }
771
641
  },
772
- /** `PUT /v1/content/settings` — replace language settings (admin). */
642
+ /** `PUT /content/settings` (admin). */
773
643
  putSettings: (body) => this.#request({
774
644
  method: 'PUT',
775
- path: '/v1/content/settings',
645
+ path: '/content/settings',
776
646
  body,
777
647
  }),
778
648
  };
779
- // ── RFC 0099 Trigger subscriptions (gated on capabilities.triggerBridge) ──
649
+ // ── RFC 0099 Trigger subscriptions (gated on `triggerBridge`) ─────────
780
650
  triggerSubscriptions = {
781
- /** `POST /v1/trigger-subscriptions` — register an external-event trigger.
782
- * The `binding.secret*` is returned ONCE at creation (SR-1); persist it.
783
- * Throws the typed `WopError` on `400`/`401`/`403`, or `501` when the host
784
- * doesn't advertise the trigger-bridge ingestion surface. */
651
+ /** `POST /trigger-subscriptions` — the `binding.secret*` is returned ONCE; persist it. */
785
652
  create: (body) => this.#request({
786
653
  method: 'POST',
787
- path: '/v1/trigger-subscriptions',
654
+ path: '/trigger-subscriptions',
788
655
  body,
789
656
  }),
790
657
  };
791
- // ── Agent workspace files (RFC 0059; gated on capabilities.workspace) ──
792
- workspace = {
793
- /**
794
- * RFC 0059 — list workspace file metadata (no bodies) for the caller's
795
- * `{tenant, workspace}`. Optional `prefix` filters the flat `path`
796
- * namespace. Returns `null` when the host doesn't advertise
797
- * `capabilities.workspace.supported` (501), so callers can branch on
798
- * capability discovery without try/catch.
799
- */
800
- listFiles: async (opts = {}) => {
801
- const search = new URLSearchParams();
802
- if (opts.prefix !== undefined)
803
- search.set('prefix', opts.prefix);
804
- const qs = search.toString();
805
- try {
806
- const res = await this.#request({
807
- method: 'GET',
808
- path: `/v1/host/workspace/files${qs ? `?${qs}` : ''}`,
809
- });
810
- return res.files;
811
- }
812
- catch (err) {
813
- if (err instanceof WopError && err.status === 501)
814
- return null;
815
- throw err;
816
- }
817
- },
818
- /**
819
- * RFC 0059 — read one workspace file. Pass `version` for a historical
820
- * snapshot when `capabilities.workspace.versioned`. Returns `null` when
821
- * the file is absent (404) or the host doesn't advertise the capability
822
- * (501).
823
- */
824
- getFile: async (path, opts = {}) => {
825
- const search = new URLSearchParams();
826
- if (opts.version !== undefined)
827
- search.set('version', String(opts.version));
828
- const qs = search.toString();
829
- try {
830
- return await this.#request({
831
- method: 'GET',
832
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}${qs ? `?${qs}` : ''}`,
833
- });
834
- }
835
- catch (err) {
836
- if (err instanceof WopError && (err.status === 404 || err.status === 501))
837
- return null;
838
- throw err;
839
- }
840
- },
841
- /**
842
- * RFC 0059 — atomic create/replace of a workspace file. Pass `ifMatch`
843
- * (the file's current `etag`) for optimistic concurrency; a stale token
844
- * throws a `WopError` with status `409` (`workspace_conflict`). Content
845
- * beyond `capabilities.workspace.maxFileBytes` throws `413`
846
- * (`workspace_too_large`). Returns the persisted `WorkspaceFile`.
847
- */
848
- putFile: (path, body, opts = {}) => {
849
- const headers = this.#mutationHeaders(opts);
850
- if (opts.ifMatch !== undefined)
851
- headers['If-Match'] = opts.ifMatch;
852
- return this.#request({
853
- method: 'PUT',
854
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
855
- body,
856
- headers,
857
- });
858
- },
859
- /**
860
- * RFC 0059 — delete a workspace file. Returns `true` on success (`204`),
861
- * `false` when the file is absent (404) or the host doesn't advertise the
862
- * capability (501).
863
- */
864
- deleteFile: async (path, opts = {}) => {
865
- try {
866
- await this.#request({
867
- method: 'DELETE',
868
- path: `/v1/host/workspace/files/${encodeURIComponent(path)}`,
869
- headers: this.#mutationHeaders(opts),
870
- });
871
- return true;
872
- }
873
- catch (err) {
874
- if (err instanceof WopError && (err.status === 404 || err.status === 501))
875
- return false;
876
- throw err;
877
- }
878
- },
879
- };
880
658
  // ── Internals ────────────────────────────────────────────────────────
659
+ #streamContext() {
660
+ return {
661
+ baseUrl: this.#baseUrl,
662
+ apiKey: this.#apiKey,
663
+ protocolVersion: this.#versionHeader,
664
+ fetch: this.#fetch,
665
+ };
666
+ }
881
667
  #mutationHeaders(opts) {
882
668
  const h = {};
883
669
  if (opts.idempotencyKey)
884
670
  h['Idempotency-Key'] = opts.idempotencyKey;
885
671
  if (opts.dedup)
886
- h['X-Dedup'] = opts.dedup;
672
+ h['OpenWOP-Dedup'] = opts.dedup;
887
673
  return h;
888
674
  }
889
675
  async #request(opts, authenticated = true) {
890
676
  const url = `${this.#baseUrl}${opts.path}`;
891
677
  const headers = {
892
678
  Accept: 'application/json',
679
+ // RFC 0172 §A.3 — on every request, authenticated or not.
680
+ 'OpenWOP-Version': this.#versionHeader,
893
681
  ...(opts.headers ?? {}),
894
682
  };
895
683
  if (opts.body !== undefined && headers['Content-Type'] === undefined) {
@@ -898,7 +686,7 @@ export class OpenwopClient {
898
686
  if (authenticated) {
899
687
  headers.Authorization = `Bearer ${this.#apiKey}`;
900
688
  }
901
- if (this.#acceptLanguage) {
689
+ if (this.#acceptLanguage && headers['Accept-Language'] === undefined) {
902
690
  headers['Accept-Language'] = this.#acceptLanguage;
903
691
  }
904
692
  const init = { method: opts.method, headers };
@@ -910,17 +698,15 @@ export class OpenwopClient {
910
698
  }
911
699
  const res = await this.#fetch(url, init);
912
700
  const text = await res.text();
913
- // Capture traceparent for error reporting per observability.md
914
- // §Trace context propagation. Header name is case-insensitive per
915
- // RFC 9110; fetch normalizes to lowercase but be defensive.
701
+ // Capture traceparent for error reporting (observability.md §Trace
702
+ // context propagation). Header names are case-insensitive per RFC 9110.
916
703
  const traceparent = res.headers.get('traceparent') ?? res.headers.get('Traceparent') ?? undefined;
917
704
  if (!res.ok) {
918
705
  let env;
919
706
  try {
920
707
  const parsed = text.length > 0 ? JSON.parse(text) : undefined;
921
- if (parsed && typeof parsed === 'object' && 'error' in parsed && 'message' in parsed) {
708
+ if (isErrorEnvelope(parsed))
922
709
  env = parsed;
923
- }
924
710
  }
925
711
  catch {
926
712
  // not JSON; leave envelope undefined
@@ -934,10 +720,17 @@ export class OpenwopClient {
934
720
  }
935
721
  catch {
936
722
  throw new WopError(res.status, text, {
937
- error: 'invalid_json',
723
+ error: 'internal_error',
938
724
  message: 'Server returned non-JSON body for a 2xx response',
725
+ details: { sdk: 'invalid_json' },
939
726
  }, traceparent);
940
727
  }
941
728
  }
942
729
  }
730
+ function isErrorEnvelope(value) {
731
+ if (typeof value !== 'object' || value === null)
732
+ return false;
733
+ const rec = value;
734
+ return typeof rec['error'] === 'string' && typeof rec['message'] === 'string';
735
+ }
943
736
  //# sourceMappingURL=client.js.map