@t4h.framework/sftp 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,369 @@
1
+ ---
2
+ name: framework-sftp
3
+ description: >-
4
+ Guides correct use of @t4h.framework/sftp in T4H Framework workflows: SFTP
5
+ SyncActivities, SftpClaim runtime dependency, the reconciler-aware Sftp client,
6
+ Binary file payloads, and local testing with Claim.run and History.run. Use when
7
+ implementing SFTP upload/download/list in workflows, wiring SftpClaim providers,
8
+ mocking SFTP in activity unit tests, working in packages/sftp, or integrating
9
+ remote file transfer (sftp:// connection strings, put/get, mkdir, stat, exists).
10
+ ---
11
+
12
+ # @t4h.framework/sftp
13
+
14
+ SFTP **activities**, **`SftpClaim`**, and a high-level **`Sftp`** client for replayable remote file operations in workflows. Package path: `framework/packages/sftp`. Peer dependency: `@t4h.framework/core` (`Binary`, `SyncActivity`, `Claim`, `History`).
15
+
16
+ Import surface (from `src/sftp.ts` only — do not reference unexported modules):
17
+
18
+ ```typescript
19
+ import {
20
+ Sftp,
21
+ SftpClaim,
22
+ SftpAppendActivity,
23
+ SftpChmodActivity,
24
+ SftpCopyActivity,
25
+ SftpCwdActivity,
26
+ SftpDeleteActivity,
27
+ SftpDownloadDirActivity,
28
+ SftpExistsActivity,
29
+ SftpGetActivity,
30
+ SftpListActivity,
31
+ SftpMkdirActivity,
32
+ SftpPutActivity,
33
+ SftpRealPathActivity,
34
+ SftpRenameActivity,
35
+ SftpRmdirActivity,
36
+ SftpStatActivity,
37
+ type SftpAppendActivityInput,
38
+ type SftpChmodActivityInput,
39
+ type SftpCopyActivityInput,
40
+ type SftpCwdActivityInput,
41
+ type SftpDeleteActivityInput,
42
+ type SftpDownloadDirActivityInput,
43
+ type SftpExistsActivityInput,
44
+ type SftpExistsResult,
45
+ type SftpGetActivityInput,
46
+ type SftpFileInfo,
47
+ type SftpListActivityInput,
48
+ type SftpMkdirActivityInput,
49
+ type SftpPutActivityInput,
50
+ type SftpRealPathActivityInput,
51
+ type SftpRenameActivityInput,
52
+ type SftpRmdirActivityInput,
53
+ type SftpStatActivityInput,
54
+ type SftpStat,
55
+ } from '@t4h.framework/sftp'
56
+
57
+ import type { Binary } from '@t4h.framework/core'
58
+ ```
59
+
60
+ Node `>=22`. See [README.md](../../../README.md).
61
+
62
+ ---
63
+
64
+ ## Architecture
65
+
66
+ `Workflow` → **`Sftp`** (`History.bind` per activity) → **`SyncActivity.run`** → **`SftpClaim.<method>(input)`**.
67
+
68
+ Every activity:
69
+
70
+ - Extends **`SyncActivity`** with a private **`Claim({ sftp: SftpClaim })`** keyed by a unique `Symbol`.
71
+ - **`toInput`** maps `(sftp: Sftp, ...args)` → serializable input including **`connectionString: sftp.connectionString`**.
72
+ - **`run`** delegates to **`this[CLAIMS_KEY].sftp.<method>(payload)`** (claim method name matches activity: `get`, `put`, `delete`, etc.).
73
+ - Void activities: **`toOutput()`** returns **`undefined`**.
74
+
75
+ `Sftp` stores **`public readonly connectionString`** and forwards each method to the bound activity reconciler.
76
+
77
+ ---
78
+
79
+ ## Claims
80
+
81
+ ### SftpClaim (required)
82
+
83
+ Runtime must register a concrete implementation:
84
+
85
+ ```typescript
86
+ { provide: SftpClaim, value: new MySftpClaimImpl() }
87
+ ```
88
+
89
+ Abstract contract (each method accepts the corresponding `*ActivityInput` type):
90
+
91
+ | Method | Input type | Return |
92
+ |--------|------------|--------|
93
+ | `append` | `SftpAppendActivityInput` | `void` |
94
+ | `chmod` | `SftpChmodActivityInput` | `void` |
95
+ | `copy` | `SftpCopyActivityInput` | `void` |
96
+ | `cwd` | `SftpCwdActivityInput` | `string` |
97
+ | `delete` | `SftpDeleteActivityInput` | `void` |
98
+ | `downloadDir` | `SftpDownloadDirActivityInput` | `Binary` |
99
+ | `exists` | `SftpExistsActivityInput` | `SftpExistsResult` |
100
+ | `get` | `SftpGetActivityInput` | `Binary` |
101
+ | `list` | `SftpListActivityInput` | `SftpFileInfo[]` |
102
+ | `mkdir` | `SftpMkdirActivityInput` | `void` |
103
+ | `put` | `SftpPutActivityInput` | `void` |
104
+ | `realPath` | `SftpRealPathActivityInput` | `string` |
105
+ | `rename` | `SftpRenameActivityInput` | `void` |
106
+ | `rmdir` | `SftpRmdirActivityInput` | `void` |
107
+ | `stat` | `SftpStatActivityInput` | `SftpStat` |
108
+
109
+ Methods may return sync values or `Promise`s.
110
+
111
+ ---
112
+
113
+ ## Sftp client (preferred in workflows)
114
+
115
+ Instantiate with a connection string (tests/README use `sftp://user:pass@host:22`):
116
+
117
+ ```typescript
118
+ import { Sftp } from '@t4h.framework/sftp'
119
+
120
+ const sftp = new Sftp('sftp://user:pass@sftp.example.com:22')
121
+ ```
122
+
123
+ All calls must run inside **`History.run`** (workflow runtime / `WorkflowTesting` provide this). Outside history, reconciler throws **`PushActivity`**.
124
+
125
+ ### Methods
126
+
127
+ | Method | Args | Returns |
128
+ |--------|------|---------|
129
+ | `append` | `filepath`, `content: Binary` | `void` |
130
+ | `chmod` | `path`, `mode: string \| number` | `void` |
131
+ | `copy` | `srcPath`, `destPath` | `void` |
132
+ | `cwd` | — | `string` |
133
+ | `delete` | `path`, `noErrorOK?: boolean` | `void` |
134
+ | `downloadDir` | `path` | `Binary` |
135
+ | `exists` | `path` | `SftpExistsResult` |
136
+ | `get` | `filepath` | `Binary` |
137
+ | `list` | `path` | `SftpFileInfo[]` |
138
+ | `mkdir` | `path`, `recursive?: boolean` | `void` |
139
+ | `put` | `filepath`, `content: Binary` | `void` |
140
+ | `realPath` | `path` | `string` |
141
+ | `rename` | `srcPath`, `destPath` | `void` |
142
+ | `rmdir` | `path`, `recursive?: boolean` | `void` |
143
+ | `stat` | `path` | `SftpStat` |
144
+
145
+ **Flags:**
146
+
147
+ - **`delete(path, true)`** — `noErrorOK`: suppress error when file missing.
148
+ - **`mkdir(path, true)`** — `recursive`: create intermediate directories.
149
+ - **`rmdir(path, true)`** — `recursive`: remove directory tree.
150
+
151
+ **`exists`:** `false` if not found; otherwise `'d'` (directory), `'-'` (file), `'l'` (symlink).
152
+
153
+ ### Example workflow usage
154
+
155
+ ```typescript
156
+ import { Workflow } from '@t4h.framework/core'
157
+ import { Sftp } from '@t4h.framework/sftp'
158
+
159
+ const sftp = new Sftp(process.env.SFTP_URL!)
160
+
161
+ export const ingestRemoteFile = new Workflow(async () => {
162
+ const remotePath = '/incoming/report.csv'
163
+
164
+ if ((await sftp.exists(remotePath)) === false) {
165
+ return { ok: false, reason: 'missing' }
166
+ }
167
+
168
+ const file = await sftp.get(remotePath)
169
+ await sftp.rename(remotePath, '/processed/report.csv')
170
+
171
+ return { ok: true, size: file /* use Binary APIs from core */ }
172
+ })
173
+ ```
174
+
175
+ Use **`Binary`** from `@t4h.framework/core` for upload/download content (`put`, `get`, `append`, `downloadDir`).
176
+
177
+ ---
178
+
179
+ ## Activities (direct reconciler)
180
+
181
+ Prefer **`Sftp`** unless you need the activity class explicitly (custom wrappers, tests asserting activity type).
182
+
183
+ ```typescript
184
+ import { History } from '@t4h.framework/core'
185
+ import { Sftp, SftpGetActivity } from '@t4h.framework/sftp'
186
+
187
+ const sftp = new Sftp('sftp://user:pass@host:22')
188
+ const content = await History.reconciler(SftpGetActivity, sftp, '/remote/file.txt')
189
+ ```
190
+
191
+ Activity ↔ client mapping:
192
+
193
+ | Activity | Client method |
194
+ |----------|---------------|
195
+ | `SftpAppendActivity` | `append` |
196
+ | `SftpChmodActivity` | `chmod` |
197
+ | `SftpCopyActivity` | `copy` |
198
+ | `SftpCwdActivity` | `cwd` |
199
+ | `SftpDeleteActivity` | `delete` |
200
+ | `SftpDownloadDirActivity` | `downloadDir` |
201
+ | `SftpExistsActivity` | `exists` |
202
+ | `SftpGetActivity` | `get` |
203
+ | `SftpListActivity` | `list` |
204
+ | `SftpMkdirActivity` | `mkdir` |
205
+ | `SftpPutActivity` | `put` |
206
+ | `SftpRealPathActivity` | `realPath` |
207
+ | `SftpRenameActivity` | `rename` |
208
+ | `SftpRmdirActivity` | `rmdir` |
209
+ | `SftpStatActivity` | `stat` |
210
+
211
+ ---
212
+
213
+ ## Activity input shapes
214
+
215
+ All inputs include **`connectionString: string`**. Path field names differ by operation:
216
+
217
+ | Input type | Fields (beyond `connectionString`) |
218
+ |------------|-----------------------------------|
219
+ | `SftpAppendActivityInput` | `filepath`, `content: Binary` |
220
+ | `SftpChmodActivityInput` | `path`, `mode: string \| number` |
221
+ | `SftpCopyActivityInput` | `srcPath`, `destPath` |
222
+ | `SftpCwdActivityInput` | _(none)_ |
223
+ | `SftpDeleteActivityInput` | `path`, `noErrorOK?: boolean` |
224
+ | `SftpDownloadDirActivityInput` | `path` |
225
+ | `SftpExistsActivityInput` | `path` |
226
+ | `SftpGetActivityInput` | `filepath` |
227
+ | `SftpListActivityInput` | `path` |
228
+ | `SftpMkdirActivityInput` | `path`, `recursive?: boolean` |
229
+ | `SftpPutActivityInput` | `filepath`, `content: Binary` |
230
+ | `SftpRealPathActivityInput` | `path` |
231
+ | `SftpRenameActivityInput` | `srcPath`, `destPath` |
232
+ | `SftpRmdirActivityInput` | `path`, `recursive?: boolean` |
233
+ | `SftpStatActivityInput` | `path` |
234
+
235
+ ---
236
+
237
+ ## Result types
238
+
239
+ ### `SftpExistsResult`
240
+
241
+ ```typescript
242
+ type SftpExistsResult = false | 'd' | '-' | 'l'
243
+ ```
244
+
245
+ ### `SftpFileInfo` (`list`)
246
+
247
+ ```typescript
248
+ type SftpFileInfo = {
249
+ name: string
250
+ type: 'd' | '-' | 'l'
251
+ size: number
252
+ modifyTime: number
253
+ accessTime: number
254
+ rights: { user: string; group: string; other: string }
255
+ owner: number
256
+ group: number
257
+ longname: string
258
+ }
259
+ ```
260
+
261
+ ### `SftpStat` (`stat`)
262
+
263
+ ```typescript
264
+ type SftpStat = {
265
+ mode: number
266
+ uid: number
267
+ gid: number
268
+ size: number
269
+ accessTime: number
270
+ modifyTime: number
271
+ isDirectory: boolean
272
+ isFile: boolean
273
+ isBlockDevice: boolean
274
+ isCharacterDevice: boolean
275
+ isSymbolicLink: boolean
276
+ isFIFO: boolean
277
+ isSocket: boolean
278
+ }
279
+ ```
280
+
281
+ ---
282
+
283
+ ## Testing
284
+
285
+ Run package tests: `yarn test` in `packages/sftp` (Vitest).
286
+
287
+ ### 1. Unit-test activity `run` with mocked `SftpClaim`
288
+
289
+ Pattern from `SftpGetActivity.spec.ts` / `SftpDeleteActivity.spec.ts`:
290
+
291
+ ```typescript
292
+ import { Claim } from '@t4h.framework/core'
293
+ import { describe, expect, it, vi } from 'vitest'
294
+ import { SftpClaim } from '@t4h.framework/sftp'
295
+ import { SftpGetActivity, type SftpGetActivityInput } from '@t4h.framework/sftp'
296
+
297
+ const connectionString = 'sftp://user:pass@sftp.example.com:22'
298
+ const mockGet = vi.fn().mockReturnValue('binary-data' as any)
299
+
300
+ Claim.run(
301
+ [{ provide: SftpClaim, value: { get: mockGet } as unknown as SftpClaim }],
302
+ () => {
303
+ const activity: SftpGetActivity = Reflect.construct(SftpGetActivity, [])
304
+ const input: SftpGetActivityInput = {
305
+ connectionString,
306
+ filepath: '/remote/file.txt',
307
+ }
308
+ const output = activity.run(input)
309
+ expect(mockGet).toHaveBeenCalledWith(input)
310
+ expect(output).toBe('binary-data')
311
+ },
312
+ )
313
+ ```
314
+
315
+ Test **`toInput`** with `new Sftp(connectionString)` and assert mapped fields. Test **`toOutput`** separately (pass-through for `Binary`/`string`/arrays; `undefined` for void activities).
316
+
317
+ ### 2. Assert `Sftp` schedules the correct activity
318
+
319
+ Pattern from `models/__tests__/Sftp.spec.ts`:
320
+
321
+ ```typescript
322
+ import { History, PushActivity, type Activity } from '@t4h.framework/core'
323
+ import { Sftp, SftpGetActivity } from '@t4h.framework/sftp'
324
+
325
+ async function captureReconcilerTrigger(fn: () => unknown) {
326
+ try {
327
+ await History.run([], fn)
328
+ } catch (error) {
329
+ if (error instanceof PushActivity) return error as PushActivity<Activity>
330
+ throw error
331
+ }
332
+ }
333
+
334
+ const client = new Sftp('sftp://user:pass@sftp.example.com:22')
335
+ const trigger = await captureReconcilerTrigger(() =>
336
+ client.get('/remote/file.txt'),
337
+ )
338
+ expect(trigger.activity.constructor).toBe(SftpGetActivity)
339
+ ```
340
+
341
+ Replay with canned output:
342
+
343
+ ```typescript
344
+ const result = await History.run(
345
+ [{ output: mockBinary, type: 'sync' }],
346
+ () => client.get('/remote/file.txt'),
347
+ )
348
+ expect(result).toBe(mockBinary)
349
+ ```
350
+
351
+ ### 3. Integration-test workflows
352
+
353
+ Use `WorkflowTesting.run` with a mocked `SftpClaim` provider. See **framework-core** skill for `WorkflowTesting` and claim wiring.
354
+
355
+ ---
356
+
357
+ ## Checklist for agents
358
+
359
+ 1. Prefer **`new Sftp(connectionString)`** in workflow code — not raw SSH/SFTP libraries.
360
+ 2. Never perform non-replayable SFTP I/O inside workflows — use **`Sftp`** or **`History.reconciler(Sftp*Activity, ...)`**.
361
+ 3. Ensure runtime provides **`SftpClaim`** (or pass a mock in tests).
362
+ 4. Pass **`Binary`** for `put` / `append` / `get` / `downloadDir` content — from `@t4h.framework/core`.
363
+ 5. Use **`filepath`** for get/put/append; **`path`** for most other operations; **`srcPath`/`destPath`** for copy/rename.
364
+ 6. Use **`exists`** before destructive ops when idempotency matters; use **`delete(..., true)`** for tolerant deletes.
365
+ 7. Use **`mkdir(..., true)`** / **`rmdir(..., true)`** only when recursive tree ops are intended.
366
+ 8. When adding tests, mirror **`Claim.run`** + partial `SftpClaim` mocks or **`History.run`** + **`PushActivity`** from `packages/sftp/src/**/__tests__`.
367
+ 9. Do not document or import APIs not exported from `@t4h.framework/sftp`.
368
+
369
+ See also: `packages/sftp/README.md`, `framework-core` skill for `History`, `SyncActivity`, and `WorkflowTesting`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t4h.framework/sftp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SFTP module for the T4H Framework",
5
5
  "homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/sftp",
6
6
  "bugs": "https://github.com/tech4humans-brasil/framework/issues",
@@ -20,7 +20,8 @@
20
20
  "types": "./dist/sftp.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",
@@ -30,12 +31,12 @@
30
31
  "test:watch": "vitest"
31
32
  },
32
33
  "devDependencies": {
33
- "@t4h.framework/core": "^0.3.0",
34
+ "@t4h.framework/core": "^0.6.0",
34
35
  "typescript": "^5.9.3",
35
36
  "vitest": "^4.0.18"
36
37
  },
37
38
  "peerDependencies": {
38
- "@t4h.framework/core": "^0.3.0"
39
+ "@t4h.framework/core": "^0.6.0"
39
40
  },
40
41
  "packageManager": "yarn@4.12.0",
41
42
  "engines": {