@t4h.framework/http 0.3.0 → 0.4.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.
@@ -0,0 +1,335 @@
1
+ ---
2
+ name: framework-http
3
+ description: >-
4
+ Guides correct use of @t4h.framework/http in T4H Framework workflows: the
5
+ `http` / `HttpClient` client, claims (HttpClientRequestClaim, FileSystemClaim,
6
+ CacheClaim for OAuth), request/response bodies, and auth. Use when making or
7
+ testing HTTP calls in workflows, wiring HTTP claim providers, OAuth2/Basic
8
+ auth, or working in packages/http.
9
+ ---
10
+
11
+ # @t4h.framework/http
12
+
13
+ HTTP activities, claims, and a reconciler-aware client for workflows. Package path: `framework/packages/http`. Peer dependencies: `@t4h.framework/core`, `@t4h.framework/fs`, `@t4h.framework/cache` (OAuth).
14
+
15
+ Import surface (from `src/http.ts` only — do not reference unexported modules such as `src/errors/HttpError.ts`):
16
+
17
+ ```typescript
18
+ import {
19
+ http,
20
+ HttpClient,
21
+ HttpClientRequestClaim,
22
+ HttpAuth,
23
+ OAuth2,
24
+ OAuth2ClientCredentials,
25
+ OAuth2GrantResourceOwnerPassword,
26
+ Body,
27
+ Response,
28
+ type HttpClientRequestClaimInput,
29
+ type HttpClientRequestClaimOutput,
30
+ } from '@t4h.framework/http'
31
+ ```
32
+
33
+ > Workflows use the **`http` / `HttpClient`** client. The package also exports
34
+ > the underlying `HttpRequestActivity` / `AbstractHttpRequestActivity`, but
35
+ > workflow code never imports, instantiates, or extends them.
36
+
37
+ ## Architecture
38
+
39
+ `Workflow` → `http`/`HttpClient` (`History.bind(HttpRequestActivity)`) → `AbstractHttpRequestActivity.run` → `HttpClientRequestClaim.request` + `FileSystemClaim.write` (response → `Body`/`Response`).
40
+
41
+ ---
42
+
43
+ ## Claims
44
+
45
+ ### HttpClientRequestClaim (required for HTTP)
46
+
47
+ Runtime must register an implementation:
48
+
49
+ ```typescript
50
+ { provide: HttpClientRequestClaim, value: new MyHttpClientRequestClaimImpl() }
51
+ ```
52
+
53
+ Contract:
54
+
55
+ ```typescript
56
+ abstract class HttpClientRequestClaim {
57
+ abstract request(
58
+ input: HttpClientRequestClaimInput,
59
+ ): HttpClientRequestClaimOutput | Promise<HttpClientRequestClaimOutput>
60
+ }
61
+
62
+ // HttpClientRequestClaimInput
63
+ {
64
+ url: URL
65
+ method: HttpMethod // required on claim input
66
+ body: Buffer | FormData | Readable | URLSearchParams | undefined
67
+ headers: Record<string, string | string[] | undefined>
68
+ redirect: 'follow' | 'manual' | number
69
+ retry: boolean | HttpRequestActivityInputRetryOptions
70
+ }
71
+
72
+ // HttpClientRequestClaimOutput
73
+ {
74
+ status: number
75
+ headers: Record<string, string | string[] | undefined>
76
+ body: Readable
77
+ }
78
+ ```
79
+
80
+ The runtime provides a concrete implementation; workflow tests mock it (see **Testing** below).
81
+
82
+ ### FileSystemClaim (required by the HTTP activity)
83
+
84
+ The HTTP activity persists the response body to `fs` (path `\`${uuid}-http-request-activity-response-body.${ext}\``, extension from `content-type` via `mime`, or `.bin`) and returns it wrapped in a `Body`. So any workflow that makes an HTTP call also needs a `FileSystemClaim` provider; tests mock its `write` to return a `Binary`.
85
+
86
+ ### CacheClaim (OAuth only)
87
+
88
+ `HttpAuth` / `OAuth2` use `CacheClaim` for token caching. OAuth2 stores tokens at `oauth2:token:${clientId}` with TTL derived from `expires_in` (90% of lifetime).
89
+
90
+ ---
91
+
92
+ ## HttpRequestActivityInput
93
+
94
+ Shared by activities and `HttpClient` config (minus `url` / `baseUrl` on client methods):
95
+
96
+ | Field | Type | Default / notes |
97
+ |-------|------|-----------------|
98
+ | `url` | `string` | Resolved with `new URL(url, baseUrl)` |
99
+ | `baseUrl` | `string?` | Optional base for relative paths |
100
+ | `method` | `HttpMethod?` | `'GET'` |
101
+ | `body` | `{ json }` \| `{ text }` \| `{ form }` | See below |
102
+ | `headers` | `Record<...>?` | Merged with body-derived headers; `auth` sets `Authorization` and overrides a custom `Authorization` header |
103
+ | `auth` | `HttpAuth` \| `{ username, password }` | Basic auth object or subclass with `getAuthorizationHeader()` |
104
+ | `retry` | `boolean` \| retry options | `true` |
105
+ | `redirect` | `'follow' \| 'manual' \| number` | `'follow'` |
106
+
107
+ **Body shapes** (handled in `AbstractHttpRequestActivity.getContent`):
108
+
109
+ - `{ json: Serializable }` → `application/json` + `Content-Length`
110
+ - `{ text: string }` → `text/plain` + `Content-Length`
111
+ - `{ form: HttpRequestActivityInputBodyFormData }` → `FormData` (nested keys as `parent[key]`; `Binary` fields get generated filenames; `undefined` / `null` skipped)
112
+
113
+ Invalid body shape throws `Invalid body`.
114
+
115
+ **Form values:** `string | number | boolean | null | undefined | Binary | nested object | readonly array of nested form data`.
116
+
117
+ ---
118
+
119
+ ## Making HTTP calls in a workflow
120
+
121
+ Always use the ready **`http` / `HttpClient`** helper — it is what the package
122
+ exports for workflows and it drives the underlying HTTP activity through the
123
+ reconciler for you (replay-safe). Never define, extend, or hand-instantiate an
124
+ activity in workflow code.
125
+
126
+ For a typed result, read the response and map with plain TypeScript:
127
+
128
+ ```typescript
129
+ const res = await http.get(`${environments.SERVICE_URL}/data`)
130
+ const data = await res.body.json<MyType>()
131
+ ```
132
+
133
+ See **HttpClient and `http`** and **Response and Body** below.
134
+
135
+ ---
136
+
137
+ ## HttpClient and `http`
138
+
139
+ `HttpClient` wraps `HttpRequestActivity` via `History.bind`. Calls inside a workflow are **reconciler-driven** (replay-safe); they throw `PushActivity` when history needs a new step.
140
+
141
+ ```typescript
142
+ import { http } from '@t4h.framework/http'
143
+
144
+ // GET
145
+ const response = await http.get('https://api.example.com/users')
146
+ const users = await response.body.json()
147
+
148
+ // POST JSON
149
+ await http.post('https://api.example.com/users', {
150
+ json: { name: 'John', email: 'john@example.com' },
151
+ })
152
+
153
+ // Base URL client (does not mutate singleton)
154
+ const api = http.extend({ baseUrl: 'https://api.example.com' })
155
+ const list = await api.get('/users')
156
+
157
+ // Instance with default auth
158
+ const client = new HttpClient({
159
+ baseUrl: 'https://api.example.com',
160
+ auth: { username: 'user', password: 'pass' },
161
+ })
162
+ ```
163
+
164
+ Methods: `request(url, config?)`, `get`, `post(url, body, config?)`, `put`, `patch`, `delete`. Per-request `auth` in config merges over client `config.auth`.
165
+
166
+ Workflow example:
167
+
168
+ ```typescript
169
+ import { Workflow } from '@t4h.framework/core'
170
+ import { http } from '@t4h.framework/http'
171
+
172
+ export const simple = new Workflow({ id: 'simple' }, async () => {
173
+ await http.post('https://example.com/webhook', {
174
+ json: { status: 'ok' },
175
+ })
176
+ })
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Response and Body
182
+
183
+ `HttpRequestActivity` exposes `Response`:
184
+
185
+ ```typescript
186
+ response.status // number
187
+ response.headers // Record<string, string | string[] | undefined>
188
+ response.body // Body instance
189
+ ```
190
+
191
+ `Body` (backed by `Binary` from filesystem claim):
192
+
193
+ ```typescript
194
+ response.body.contentType
195
+ response.body.contentLength
196
+ await response.body.text()
197
+ await response.body.json<T>() // only if content-type matches */json or */*+json
198
+ await response.body.stream()
199
+ ```
200
+
201
+ `body.json()` throws `Invalid content type` for non-JSON types (e.g. `text/plain`, `text/html`).
202
+
203
+ `Body` extends `SerializableClass` (`packages/http/src/models/Body.ts`) and is
204
+ persisted through `FileSystemClaim`, so the `Response`/`Body` returned by the
205
+ `http` client **is replay-safe** — it is serialized between reconciler steps and
206
+ rebuilt on replay. You do **not** need a custom activity to "flatten" the body
207
+ into history; read it with `await res.body.json<T>()` in the workflow and
208
+ transform with plain TypeScript.
209
+
210
+ Serializable shape: `{ binary: ... }` via `toSerializable` / `fromSerializable`.
211
+
212
+ ---
213
+
214
+ ## Authentication
215
+
216
+ ### Basic (inline)
217
+
218
+ ```typescript
219
+ await http.get('https://api.example.com/data', {
220
+ auth: { username: 'user', password: 'pass' },
221
+ })
222
+ ```
223
+
224
+ ### Custom HttpAuth
225
+
226
+ Subclass `HttpAuth` (extends `SerializableClass`), implement `getAuthorizationHeader()`, and optional `toSerializable` / `fromSerializable` for workflow persistence.
227
+
228
+ ### OAuth2 client credentials
229
+
230
+ ```typescript
231
+ import { http, OAuth2ClientCredentials } from '@t4h.framework/http'
232
+
233
+ const auth = new OAuth2ClientCredentials({
234
+ tokenUrl: 'https://auth.example.com/token',
235
+ clientId: 'my-client-id',
236
+ clientSecret: 'my-client-secret',
237
+ })
238
+
239
+ await http.get('https://api.example.com/data', { auth })
240
+ ```
241
+
242
+ ### OAuth2 resource owner password
243
+
244
+ ```typescript
245
+ import { OAuth2GrantResourceOwnerPassword } from '@t4h.framework/http'
246
+
247
+ const auth = new OAuth2GrantResourceOwnerPassword({
248
+ tokenUrl: 'https://auth.example.com/token',
249
+ clientId: 'my-client-id',
250
+ clientSecret: 'my-client-secret',
251
+ username: 'user',
252
+ password: 'pass',
253
+ })
254
+ ```
255
+
256
+ OAuth2 flow: POST `application/x-www-form-urlencoded` to `tokenUrl` via `HttpClientRequestClaim`; cache token; refresh with `grant_type=refresh_token` when expired if `refresh_token` present; header format `${token_type} ${access_token}`.
257
+
258
+ Requires `CacheClaim` + `HttpClientRequestClaim` in the runtime.
259
+
260
+ ---
261
+
262
+ ## Testing
263
+
264
+ Test the **workflow** with `TestWorkflowEnvironment` from
265
+ `@t4h.framework/core/testing`, mocking the two claims the HTTP activity uses:
266
+ `HttpClientRequestClaim` (the network request) and `FileSystemClaim` (where the
267
+ activity persists the response body). See the **framework-workflow-testing** skill
268
+ for the full harness API.
269
+
270
+ ```typescript
271
+ import { Readable } from 'node:stream'
272
+ import { Binary } from '@t4h.framework/core'
273
+ import { TestWorkflowEnvironment, mockClaim } from '@t4h.framework/core/testing'
274
+ import { HttpClientRequestClaim } from '@t4h.framework/http'
275
+ import { FileSystemClaim } from '@t4h.framework/fs'
276
+
277
+ // Minimal Binary the FileSystemClaim mock returns from write().
278
+ class MockBinary extends Binary {
279
+ public readonly contentType = 'application/json'
280
+ public readonly contentLength: number
281
+
282
+ constructor(private readonly data: Buffer) {
283
+ super()
284
+ this.contentLength = data.byteLength
285
+ }
286
+
287
+ public stream() {
288
+ return Readable.from(this.data)
289
+ }
290
+ }
291
+
292
+ const body = Buffer.from(JSON.stringify({ ok: true }))
293
+
294
+ // mockImplementation (not mockResolvedValue): replay runs the workflow twice, so
295
+ // each call must hand back a fresh, unconsumed stream.
296
+ const mockRequest = vi.fn().mockImplementation(async () => ({
297
+ status: 200,
298
+ headers: { 'content-type': 'application/json' },
299
+ body: Readable.from(body),
300
+ }))
301
+
302
+ const mockWrite = vi.fn().mockImplementation(async () => new MockBinary(body))
303
+
304
+ const env = TestWorkflowEnvironment.create({ now: 0 })
305
+
306
+ const output = await env.execute(workflow, input, {
307
+ claims: [
308
+ mockClaim(HttpClientRequestClaim, { request: mockRequest }),
309
+ mockClaim(FileSystemClaim, { write: mockWrite, read: vi.fn() }),
310
+ ],
311
+ env: { SERVICE_URL: 'https://api.example.com' },
312
+ })
313
+
314
+ // request input carries a URL object
315
+ expect(mockRequest.mock.calls[0][0].url).toEqual(
316
+ new URL('https://api.example.com/data'),
317
+ )
318
+ ```
319
+
320
+ Inspect `mockRequest.mock.calls` for URLs, methods, and bodies.
321
+
322
+ ---
323
+
324
+ ## Checklist for agents
325
+
326
+ 1. In workflows, **always** use **`http` / `HttpClient`** — never define or extend an activity. Get typed, replay-safe results with `await res.body.json<T>()` plus plain TypeScript.
327
+ 2. Never call `fetch` directly in workflow code if the step must be replayable — use the package APIs.
328
+ 3. Ensure runtime provides **`HttpClientRequestClaim`** and **`FileSystemClaim`** (or pass mocks in tests).
329
+ 4. Use **`{ json: ... }`**, **`{ text: ... }`**, or **`{ form: ... }`** for bodies — not raw `Buffer` on activity input.
330
+ 5. Read responses with **`await response.body.json()`** or **`text()`**; check `status` explicitly (no automatic throw on 4xx/5xx).
331
+ 6. For OAuth, wire **`CacheClaim`** and use exported grant classes only.
332
+ 7. Test workflows with **`TestWorkflowEnvironment`** + **`mockClaim`**, mocking **`HttpClientRequestClaim`** and **`FileSystemClaim`**; assert on `mockRequest.mock.calls`.
333
+ 8. Do not document or import APIs not exported from `@t4h.framework/http`.
334
+
335
+ See also: `packages/http/README.md`; **framework-workflow-testing** for the test harness.
@@ -1,6 +1,5 @@
1
1
  import { CacheClaim } from '@t4h.framework/cache';
2
- import { Claim } from '@t4h.framework/core';
3
- import { SerializableClass } from '../../../core/dist/models/SerializableHelper.js';
2
+ import { Claim, SerializableClass } from '@t4h.framework/core';
4
3
  declare const HTTP_AUTH_CLAIMS_KEY: unique symbol;
5
4
  export declare abstract class HttpAuth extends SerializableClass {
6
5
  readonly [HTTP_AUTH_CLAIMS_KEY]: Claim<{
@@ -1 +1 @@
1
- {"version":3,"file":"HttpAuth.d.ts","sourceRoot":"","sources":["../../src/models/HttpAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAA;AAEnF,QAAA,MAAM,oBAAoB,eAA6B,CAAA;AAEvD,8BAAsB,QAAS,SAAQ,iBAAiB;IACtD,SAAgB,CAAC,oBAAoB,CAAC;;OAEpC;aAEc,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAC1D"}
1
+ {"version":3,"file":"HttpAuth.d.ts","sourceRoot":"","sources":["../../src/models/HttpAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE9D,QAAA,MAAM,oBAAoB,eAA6B,CAAA;AAEvD,8BAAsB,QAAS,SAAQ,iBAAiB;IACtD,SAAgB,CAAC,oBAAoB,CAAC;;OAEpC;aAEc,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;CAC1D"}
@@ -1,6 +1,5 @@
1
1
  import { CacheClaim } from '@t4h.framework/cache';
2
- import { Claim } from '@t4h.framework/core';
3
- import { SerializableClass } from '../../../core/dist/models/SerializableHelper.js';
2
+ import { Claim, SerializableClass } from '@t4h.framework/core';
4
3
  const HTTP_AUTH_CLAIMS_KEY = Symbol('http-auth-claims');
5
4
  export class HttpAuth extends SerializableClass {
6
5
  [HTTP_AUTH_CLAIMS_KEY] = new Claim({
@@ -1 +1 @@
1
- {"version":3,"file":"HttpAuth.js","sourceRoot":"","sources":["../../src/models/HttpAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAA;AAEnF,MAAM,oBAAoB,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAEvD,MAAM,OAAgB,QAAS,SAAQ,iBAAiB;IACtC,CAAC,oBAAoB,CAAC,GAAG,IAAI,KAAK,CAAC;QACjD,KAAK,EAAE,UAAU;KAClB,CAAC,CAAA;CAGH"}
1
+ {"version":3,"file":"HttpAuth.js","sourceRoot":"","sources":["../../src/models/HttpAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE9D,MAAM,oBAAoB,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAEvD,MAAM,OAAgB,QAAS,SAAQ,iBAAiB;IACtC,CAAC,oBAAoB,CAAC,GAAG,IAAI,KAAK,CAAC;QACjD,KAAK,EAAE,UAAU;KAClB,CAAC,CAAA;CAGH"}
@@ -0,0 +1,3 @@
1
+ import { HttpClientRequestClaim } from '../models/HttpClientRequestClaim.js';
2
+ export declare function mockHttpClaim(implementation?: Partial<HttpClientRequestClaim>): import("@t4h.framework/core").ClaimProvider<HttpClientRequestClaim>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,sBAAsB,EAEvB,MAAM,qCAAqC,CAAA;AAE5C,wBAAgB,aAAa,CAC3B,cAAc,GAAE,OAAO,CAAC,sBAAsB,CAAM,uEAYrD"}
@@ -0,0 +1,15 @@
1
+ import { Readable } from 'node:stream';
2
+ import { mockClaim } from '@t4h.framework/core/testing';
3
+ import { HttpClientRequestClaim, } from '../models/HttpClientRequestClaim.js';
4
+ export function mockHttpClaim(implementation = {}) {
5
+ const defaultResponse = {
6
+ status: 200,
7
+ headers: { 'content-type': 'application/json' },
8
+ body: Readable.from(Buffer.from('{}')),
9
+ };
10
+ return mockClaim(HttpClientRequestClaim, {
11
+ request: async () => defaultResponse,
12
+ ...implementation,
13
+ });
14
+ }
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAEvD,OAAO,EACL,sBAAsB,GAEvB,MAAM,qCAAqC,CAAA;AAE5C,MAAM,UAAU,aAAa,CAC3B,iBAAkD,EAAE;IAEpD,MAAM,eAAe,GAAiC;QACpD,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KACvC,CAAA;IAED,OAAO,SAAS,CAAC,sBAAsB,EAAE;QACvC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,eAAe;QACpC,GAAG,cAAc;KAClB,CAAC,CAAA;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/http",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "HTTP module for the T4H Framework",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/http",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -20,7 +20,8 @@
20
20
  "types": "./dist/http.d.ts",
21
21
  "files": [
22
22
  "dist",
23
- "LICENSE"
23
+ "LICENSE",
24
+ ".ai"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsc --project tsconfig.build.json",
@@ -37,18 +38,18 @@
37
38
  "uuidv7": "^1.1.0"
38
39
  },
39
40
  "devDependencies": {
40
- "@t4h.framework/cache": "^0.3.0",
41
- "@t4h.framework/core": "^0.3.0",
42
- "@t4h.framework/fs": "^0.3.0",
41
+ "@t4h.framework/cache": "^0.5.0",
42
+ "@t4h.framework/core": "^0.8.0",
43
+ "@t4h.framework/fs": "^0.5.0",
43
44
  "@types/type-is": "^1.6.7",
44
45
  "@vitest/coverage-v8": "^4.0.18",
45
46
  "typescript": "^5.9.3",
46
47
  "vitest": "^4.0.18"
47
48
  },
48
49
  "peerDependencies": {
49
- "@t4h.framework/cache": "^0.3.0",
50
- "@t4h.framework/core": "^0.3.0",
51
- "@t4h.framework/fs": "^0.3.0"
50
+ "@t4h.framework/cache": "^0.5.0",
51
+ "@t4h.framework/core": "^0.8.0",
52
+ "@t4h.framework/fs": "^0.5.0"
52
53
  },
53
54
  "packageManager": "yarn@4.12.0",
54
55
  "engines": {