@ontrails/core 1.0.0-beta.5 → 1.0.0-beta.50

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 (252) hide show
  1. package/CHANGELOG.md +459 -14
  2. package/README.md +103 -28
  3. package/package.json +16 -1
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-derivation.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +59 -11
  13. package/src/derive.ts +271 -19
  14. package/src/detours.ts +8 -0
  15. package/src/diagnostics.ts +21 -0
  16. package/src/draft.ts +350 -0
  17. package/src/entity.ts +346 -0
  18. package/src/error-rendering.ts +87 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1172 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +569 -33
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-field-rendering.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +11 -3
  30. package/src/permits.ts +12 -0
  31. package/src/queue.ts +163 -0
  32. package/src/redaction/patterns.ts +6 -3
  33. package/src/resilience.ts +11 -11
  34. package/src/resource-config.ts +804 -0
  35. package/src/resource.ts +194 -0
  36. package/src/result.ts +18 -0
  37. package/src/run.ts +76 -0
  38. package/src/runtime-builtins.ts +69 -0
  39. package/src/schedule-runtime.ts +689 -0
  40. package/src/schedule.ts +326 -0
  41. package/src/serialization.ts +92 -55
  42. package/src/sha256.ts +136 -0
  43. package/src/signal-diagnostics.ts +633 -0
  44. package/src/signal-ref.ts +111 -0
  45. package/src/signal.ts +104 -0
  46. package/src/store/accessor-protocol.ts +56 -0
  47. package/src/store/index.ts +4 -0
  48. package/src/structured-examples.ts +248 -0
  49. package/src/surface-derivation.ts +91 -0
  50. package/src/surface-filter.ts +101 -0
  51. package/src/surface-overlay.ts +694 -0
  52. package/src/surface-versioning.ts +42 -0
  53. package/src/topo.ts +762 -38
  54. package/src/tracing.ts +346 -0
  55. package/src/trail-id-glob.ts +15 -0
  56. package/src/trail.ts +1244 -34
  57. package/src/trails/derive-trail.ts +835 -0
  58. package/src/trails/index.ts +9 -0
  59. package/src/trails/ingest.ts +152 -0
  60. package/src/trails-db.ts +212 -0
  61. package/src/transport-error-map.ts +163 -0
  62. package/src/type-utils.ts +63 -10
  63. package/src/types.ts +254 -12
  64. package/src/validate-established-topo.ts +73 -0
  65. package/src/validate-topo.ts +569 -55
  66. package/src/validation.ts +152 -4
  67. package/src/version-marker.ts +716 -0
  68. package/src/version-resolution.ts +308 -0
  69. package/src/version-runtime.ts +120 -0
  70. package/src/webhook.ts +461 -0
  71. package/src/workspace.ts +183 -16
  72. package/src/zod-wrappers.ts +72 -0
  73. package/.turbo/turbo-build.log +0 -1
  74. package/.turbo/turbo-lint.log +0 -3
  75. package/.turbo/turbo-typecheck.log +0 -1
  76. package/dist/adapters.d.ts +0 -39
  77. package/dist/adapters.d.ts.map +0 -1
  78. package/dist/adapters.js +0 -2
  79. package/dist/adapters.js.map +0 -1
  80. package/dist/blob-ref.d.ts +0 -20
  81. package/dist/blob-ref.d.ts.map +0 -1
  82. package/dist/blob-ref.js +0 -22
  83. package/dist/blob-ref.js.map +0 -1
  84. package/dist/branded.d.ts +0 -36
  85. package/dist/branded.d.ts.map +0 -1
  86. package/dist/branded.js +0 -89
  87. package/dist/branded.js.map +0 -1
  88. package/dist/collections.d.ts +0 -31
  89. package/dist/collections.d.ts.map +0 -1
  90. package/dist/collections.js +0 -60
  91. package/dist/collections.js.map +0 -1
  92. package/dist/context.d.ts +0 -10
  93. package/dist/context.d.ts.map +0 -1
  94. package/dist/context.js +0 -15
  95. package/dist/context.js.map +0 -1
  96. package/dist/derive.d.ts +0 -33
  97. package/dist/derive.d.ts.map +0 -1
  98. package/dist/derive.js +0 -125
  99. package/dist/derive.js.map +0 -1
  100. package/dist/errors.d.ts +0 -83
  101. package/dist/errors.d.ts.map +0 -1
  102. package/dist/errors.js +0 -142
  103. package/dist/errors.js.map +0 -1
  104. package/dist/event.d.ts +0 -45
  105. package/dist/event.d.ts.map +0 -1
  106. package/dist/event.js +0 -17
  107. package/dist/event.js.map +0 -1
  108. package/dist/fetch.d.ts +0 -15
  109. package/dist/fetch.d.ts.map +0 -1
  110. package/dist/fetch.js +0 -102
  111. package/dist/fetch.js.map +0 -1
  112. package/dist/guards.d.ts +0 -17
  113. package/dist/guards.d.ts.map +0 -1
  114. package/dist/guards.js +0 -25
  115. package/dist/guards.js.map +0 -1
  116. package/dist/health.d.ts +0 -18
  117. package/dist/health.d.ts.map +0 -1
  118. package/dist/health.js +0 -5
  119. package/dist/health.js.map +0 -1
  120. package/dist/index.d.ts +0 -36
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -39
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -16
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -66
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -89
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -18
  203. package/dist/trail.js.map +0 -1
  204. package/dist/type-utils.d.ts +0 -14
  205. package/dist/type-utils.d.ts.map +0 -1
  206. package/dist/type-utils.js +0 -12
  207. package/dist/type-utils.js.map +0 -1
  208. package/dist/types.d.ts +0 -46
  209. package/dist/types.d.ts.map +0 -1
  210. package/dist/types.js +0 -2
  211. package/dist/types.js.map +0 -1
  212. package/dist/validate-topo.d.ts +0 -24
  213. package/dist/validate-topo.d.ts.map +0 -1
  214. package/dist/validate-topo.js +0 -158
  215. package/dist/validate-topo.js.map +0 -1
  216. package/dist/validation.d.ts +0 -27
  217. package/dist/validation.d.ts.map +0 -1
  218. package/dist/validation.js +0 -134
  219. package/dist/validation.js.map +0 -1
  220. package/dist/workspace.d.ts +0 -25
  221. package/dist/workspace.d.ts.map +0 -1
  222. package/dist/workspace.js +0 -57
  223. package/dist/workspace.js.map +0 -1
  224. package/src/__tests__/blob-ref.test.ts +0 -103
  225. package/src/__tests__/branded.test.ts +0 -148
  226. package/src/__tests__/collections.test.ts +0 -126
  227. package/src/__tests__/context.test.ts +0 -66
  228. package/src/__tests__/derive.test.ts +0 -203
  229. package/src/__tests__/errors.test.ts +0 -309
  230. package/src/__tests__/event.test.ts +0 -82
  231. package/src/__tests__/fetch.test.ts +0 -217
  232. package/src/__tests__/guards.test.ts +0 -102
  233. package/src/__tests__/job.test.ts +0 -98
  234. package/src/__tests__/layer.test.ts +0 -212
  235. package/src/__tests__/path-security.test.ts +0 -114
  236. package/src/__tests__/patterns.test.ts +0 -273
  237. package/src/__tests__/redaction.test.ts +0 -244
  238. package/src/__tests__/resilience.test.ts +0 -246
  239. package/src/__tests__/result.test.ts +0 -155
  240. package/src/__tests__/serialization.test.ts +0 -401
  241. package/src/__tests__/topo.test.ts +0 -181
  242. package/src/__tests__/trail.test.ts +0 -217
  243. package/src/__tests__/type-utils.test.ts +0 -63
  244. package/src/__tests__/validate-topo.test.ts +0 -278
  245. package/src/__tests__/validation.test.ts +0 -283
  246. package/src/__tests__/workspace.test.ts +0 -183
  247. package/src/adapters.ts +0 -68
  248. package/src/event.ts +0 -77
  249. package/src/health.ts +0 -23
  250. package/src/job.ts +0 -20
  251. package/tsconfig.json +0 -9
  252. package/tsconfig.tsbuildinfo +0 -1
@@ -1,98 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { jobOutputSchema } from '../job';
6
-
7
- describe('jobOutputSchema', () => {
8
- test('parses a valid completed job output', () => {
9
- const input = {
10
- completedAt: '2026-03-25T00:01:00Z',
11
- current: 10,
12
- jobId: 'job-123',
13
- percentage: 100,
14
- result: { rows: 42 },
15
- startedAt: '2026-03-25T00:00:00Z',
16
- status: 'completed',
17
- total: 10,
18
- };
19
-
20
- const parsed = jobOutputSchema.parse(input);
21
-
22
- expect(parsed.jobId).toBe('job-123');
23
- expect(parsed.status).toBe('completed');
24
- expect(parsed.current).toBe(10);
25
- expect(parsed.total).toBe(10);
26
- });
27
-
28
- test('parses a minimal pending job output', () => {
29
- const input = {
30
- current: 0,
31
- jobId: 'job-456',
32
- status: 'pending',
33
- total: 100,
34
- };
35
-
36
- const parsed = jobOutputSchema.parse(input);
37
-
38
- expect(parsed.jobId).toBe('job-456');
39
- expect(parsed.status).toBe('pending');
40
- expect(parsed.result).toBeUndefined();
41
- expect(parsed.error).toBeUndefined();
42
- });
43
-
44
- test('parses a failed job with error', () => {
45
- const input = {
46
- current: 3,
47
- error: 'connection reset',
48
- jobId: 'job-789',
49
- status: 'failed',
50
- total: 10,
51
- };
52
-
53
- const parsed = jobOutputSchema.parse(input);
54
-
55
- expect(parsed.status).toBe('failed');
56
- expect(parsed.error).toBe('connection reset');
57
- });
58
-
59
- test('rejects when jobId is missing', () => {
60
- const input = {
61
- current: 1,
62
- status: 'running',
63
- total: 5,
64
- };
65
-
66
- expect(() => jobOutputSchema.parse(input)).toThrow();
67
- });
68
-
69
- test('rejects an invalid status value', () => {
70
- const input = {
71
- current: 0,
72
- jobId: 'job-bad',
73
- status: 'unknown',
74
- total: 0,
75
- };
76
-
77
- expect(() => jobOutputSchema.parse(input)).toThrow();
78
- });
79
-
80
- test('composes statusFields and progressFields correctly', () => {
81
- const { shape } = jobOutputSchema;
82
-
83
- // Status field from statusFields()
84
- expect(shape.status).toBeInstanceOf(z.ZodEnum);
85
-
86
- // Progress fields from progressFields()
87
- expect(shape.current).toBeInstanceOf(z.ZodNumber);
88
- expect(shape.total).toBeInstanceOf(z.ZodNumber);
89
- expect(shape.percentage).toBeDefined();
90
-
91
- // Job-specific fields
92
- expect(shape.jobId).toBeInstanceOf(z.ZodString);
93
- expect(shape.error).toBeDefined();
94
- expect(shape.result).toBeDefined();
95
- expect(shape.startedAt).toBeDefined();
96
- expect(shape.completedAt).toBeDefined();
97
- });
98
- });
@@ -1,212 +0,0 @@
1
- /* oxlint-disable require-await -- adapter mocks and layer wrappers satisfy async interfaces without awaiting */
2
- import { describe, test, expect } from 'bun:test';
3
-
4
- import { z } from 'zod';
5
-
6
- import type {
7
- IndexAdapter,
8
- StorageAdapter,
9
- CacheAdapter,
10
- SearchOptions,
11
- SearchResult,
12
- StorageOptions,
13
- } from '../adapters';
14
- import type { HealthStatus, HealthResult, HealthCheck } from '../health';
15
- import { composeLayers } from '../layer';
16
- import type { Layer } from '../layer';
17
- import { Result } from '../result';
18
- import { trail } from '../trail';
19
- import type { TrailContext } from '../types';
20
-
21
- const stubCtx: TrailContext = {
22
- requestId: 'test-layer',
23
- signal: AbortSignal.timeout(5000),
24
- };
25
-
26
- const echoTrail = trail('echo', {
27
- input: z.object({ value: z.string() }),
28
- metadata: { domain: 'test' },
29
- output: z.object({ value: z.string() }),
30
- run: (input) => Result.ok({ value: input.value }),
31
- });
32
-
33
- // ---------------------------------------------------------------------------
34
- // Layer tests
35
- // ---------------------------------------------------------------------------
36
-
37
- describe('Layer', () => {
38
- test('single layer wraps implementation', async () => {
39
- const prefixLayer: Layer = {
40
- name: 'prefix',
41
- wrap(_trail, impl) {
42
- return async (input, ctx) => {
43
- const result = await impl(input, ctx);
44
- return result.map((out) => ({
45
- ...out,
46
- value: `prefixed:${(out as { value: string }).value}`,
47
- }));
48
- };
49
- },
50
- };
51
-
52
- const wrapped = composeLayers([prefixLayer], echoTrail, echoTrail.run);
53
- const result = await wrapped({ value: 'hello' }, stubCtx);
54
-
55
- expect(result.isOk()).toBe(true);
56
- expect(result.unwrap()).toEqual({ value: 'prefixed:hello' });
57
- });
58
-
59
- test('multiple layers compose in outermost-first order', async () => {
60
- const log: string[] = [];
61
-
62
- const outer: Layer = {
63
- name: 'outer',
64
- wrap(_trail, impl) {
65
- return async (input, ctx) => {
66
- log.push('outer:before');
67
- const r = await impl(input, ctx);
68
- log.push('outer:after');
69
- return r;
70
- };
71
- },
72
- };
73
-
74
- const inner: Layer = {
75
- name: 'inner',
76
- wrap(_trail, impl) {
77
- return async (input, ctx) => {
78
- log.push('inner:before');
79
- const r = await impl(input, ctx);
80
- log.push('inner:after');
81
- return r;
82
- };
83
- },
84
- };
85
-
86
- const wrapped = composeLayers([outer, inner], echoTrail, echoTrail.run);
87
- await wrapped({ value: 'x' }, stubCtx);
88
-
89
- expect(log).toEqual([
90
- 'outer:before',
91
- 'inner:before',
92
- 'inner:after',
93
- 'outer:after',
94
- ]);
95
- });
96
-
97
- test('layer can short-circuit without calling inner implementation', async () => {
98
- const shortCircuit: Layer = {
99
- name: 'short-circuit',
100
- wrap() {
101
- return async () => Result.err(new Error('blocked'));
102
- },
103
- };
104
-
105
- const wrapped = composeLayers([shortCircuit], echoTrail, echoTrail.run);
106
- const result = await wrapped({ value: 'hello' }, stubCtx);
107
-
108
- expect(result.isErr()).toBe(true);
109
- const err = result as unknown as { error: Error };
110
- expect(err.error.message).toBe('blocked');
111
- });
112
-
113
- test('layer can inspect trail metadata', () => {
114
- let capturedDomain: unknown;
115
-
116
- const inspectLayer: Layer = {
117
- name: 'inspect',
118
- wrap(t, impl) {
119
- capturedDomain = t.metadata?.['domain'];
120
- return impl;
121
- },
122
- };
123
-
124
- composeLayers([inspectLayer], echoTrail, echoTrail.run);
125
-
126
- expect(capturedDomain).toBe('test');
127
- });
128
-
129
- test('empty layers array returns implementation unchanged', () => {
130
- const wrapped = composeLayers([], echoTrail, echoTrail.run);
131
- expect(wrapped).toBe(echoTrail.run);
132
- });
133
- });
134
-
135
- // ---------------------------------------------------------------------------
136
- // Health types (compile-time verification)
137
- // ---------------------------------------------------------------------------
138
-
139
- describe('health types', () => {
140
- test('HealthResult satisfies the interface', () => {
141
- const check: HealthCheck = {
142
- latency: 12,
143
- message: 'ok',
144
- status: 'healthy',
145
- };
146
- const result: HealthResult = {
147
- checks: { db: check },
148
- status: 'healthy',
149
- uptime: 3600,
150
- version: '1.0.0',
151
- };
152
-
153
- expect(result.status).toBe('healthy');
154
- expect(result.checks['db']?.status).toBe('healthy');
155
- });
156
-
157
- test('HealthStatus union is exhaustive', () => {
158
- const statuses: HealthStatus[] = ['healthy', 'degraded', 'unhealthy'];
159
- expect(statuses).toHaveLength(3);
160
- });
161
- });
162
-
163
- // ---------------------------------------------------------------------------
164
- // Adapter types (compile-time verification)
165
- // ---------------------------------------------------------------------------
166
-
167
- describe('adapter types', () => {
168
- test('IndexAdapter mock satisfies the interface', () => {
169
- const mock: IndexAdapter = {
170
- index: async () => Result.ok(),
171
- remove: async () => Result.ok(),
172
- search: async () => Result.ok([]),
173
- };
174
- expect(mock.index).toBeDefined();
175
- expect(mock.search).toBeDefined();
176
- expect(mock.remove).toBeDefined();
177
- });
178
-
179
- test('StorageAdapter mock satisfies the interface', () => {
180
- const mock: StorageAdapter = {
181
- delete: async () => Result.ok(),
182
- get: async () => Result.ok('value'),
183
- has: async () => Result.ok(true),
184
- set: async () => Result.ok(),
185
- };
186
- expect(mock.has).toBeDefined();
187
- });
188
-
189
- test('CacheAdapter mock satisfies the interface', () => {
190
- const mock: CacheAdapter = {
191
- clear: async () => Result.ok(),
192
- delete: async () => Result.ok(),
193
- get: async () => Result.ok(),
194
- set: async () => Result.ok(),
195
- };
196
- expect(mock.clear).toBeDefined();
197
- });
198
-
199
- test('SearchOptions and SearchResult satisfy their shapes', () => {
200
- const opts: SearchOptions = { filters: { tag: 'a' }, limit: 10, offset: 0 };
201
- const hit: SearchResult = {
202
- document: { title: 'x' },
203
- id: '1',
204
- score: 0.95,
205
- };
206
- const sopts: StorageOptions = { ttl: 5000 };
207
-
208
- expect(opts.limit).toBe(10);
209
- expect(hit.score).toBe(0.95);
210
- expect(sopts.ttl).toBe(5000);
211
- });
212
- });
@@ -1,114 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { resolve, join } from 'node:path';
3
-
4
- import { PermissionError } from '../errors.js';
5
- import { securePath, isPathSafe, resolveSafePath } from '../path-security.js';
6
-
7
- // ---------------------------------------------------------------------------
8
- // securePath
9
- // ---------------------------------------------------------------------------
10
-
11
- describe('securePath', () => {
12
- const base = '/project/workspace';
13
-
14
- test('resolves a simple relative path', () => {
15
- const result = securePath(base, 'src/index.ts');
16
- expect(result.isOk()).toBe(true);
17
- expect(result.unwrap()).toBe(resolve(base, 'src/index.ts'));
18
- });
19
-
20
- test('resolves nested paths', () => {
21
- const result = securePath(base, 'src/../lib/utils.ts');
22
- expect(result.isOk()).toBe(true);
23
- expect(result.unwrap()).toBe(resolve(base, 'lib/utils.ts'));
24
- });
25
-
26
- test('accepts the base directory itself', () => {
27
- const result = securePath(base, '.');
28
- expect(result.isOk()).toBe(true);
29
- expect(result.unwrap()).toBe(resolve(base));
30
- });
31
-
32
- test('rejects path traversal with ..', () => {
33
- const result = securePath(base, '../../etc/passwd');
34
- expect(result.isErr()).toBe(true);
35
- const err = result as unknown as { error: PermissionError };
36
- expect(err.error).toBeInstanceOf(PermissionError);
37
- expect(err.error.message).toContain('Path traversal detected');
38
- });
39
-
40
- test('rejects absolute paths outside base', () => {
41
- const result = securePath(base, '/etc/passwd');
42
- expect(result.isErr()).toBe(true);
43
- const err = result as unknown as { error: PermissionError };
44
- expect(err.error).toBeInstanceOf(PermissionError);
45
- });
46
-
47
- test('rejects sneaky traversal (subdir/../../..)', () => {
48
- const result = securePath(base, 'src/../../..');
49
- expect(result.isErr()).toBe(true);
50
- });
51
-
52
- test('allows paths that mention .. but stay inside', () => {
53
- const result = securePath(base, 'src/../src/file.ts');
54
- expect(result.isOk()).toBe(true);
55
- expect(result.unwrap()).toBe(resolve(base, 'src/file.ts'));
56
- });
57
- });
58
-
59
- // ---------------------------------------------------------------------------
60
- // isPathSafe
61
- // ---------------------------------------------------------------------------
62
-
63
- describe('isPathSafe', () => {
64
- const base = '/project/workspace';
65
-
66
- test('returns true for child paths', () => {
67
- expect(isPathSafe(base, 'src/index.ts')).toBe(true);
68
- });
69
-
70
- test('returns true for the base directory', () => {
71
- expect(isPathSafe(base, '.')).toBe(true);
72
- });
73
-
74
- test('returns false for traversal', () => {
75
- expect(isPathSafe(base, '../secret')).toBe(false);
76
- });
77
-
78
- test('returns false for absolute paths outside base', () => {
79
- expect(isPathSafe(base, '/tmp/evil')).toBe(false);
80
- });
81
- });
82
-
83
- // ---------------------------------------------------------------------------
84
- // resolveSafePath
85
- // ---------------------------------------------------------------------------
86
-
87
- describe('resolveSafePath', () => {
88
- const base = '/project/workspace';
89
-
90
- test('joins multiple segments safely', () => {
91
- const result = resolveSafePath(base, 'src', 'lib', 'utils.ts');
92
- expect(result.isOk()).toBe(true);
93
- expect(result.unwrap()).toBe(join(resolve(base), 'src', 'lib', 'utils.ts'));
94
- });
95
-
96
- test('rejects when segments escape the base', () => {
97
- const result = resolveSafePath(base, 'src', '../../../etc');
98
- expect(result.isErr()).toBe(true);
99
- const err = result as unknown as { error: PermissionError };
100
- expect(err.error).toBeInstanceOf(PermissionError);
101
- });
102
-
103
- test('handles single segment', () => {
104
- const result = resolveSafePath(base, 'file.txt');
105
- expect(result.isOk()).toBe(true);
106
- expect(result.unwrap()).toBe(resolve(base, 'file.txt'));
107
- });
108
-
109
- test('handles empty segments', () => {
110
- const result = resolveSafePath(base);
111
- expect(result.isOk()).toBe(true);
112
- expect(result.unwrap()).toBe(resolve(base));
113
- });
114
- });
@@ -1,273 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import {
6
- paginationFields,
7
- paginatedOutput,
8
- bulkOutput,
9
- timestampFields,
10
- dateRangeFields,
11
- sortFields,
12
- statusFields,
13
- changeOutput,
14
- progressFields,
15
- } from '../patterns/index.js';
16
-
17
- // ---------------------------------------------------------------------------
18
- // paginationFields
19
- // ---------------------------------------------------------------------------
20
-
21
- describe('paginationFields', () => {
22
- const schema = paginationFields();
23
-
24
- test('applies defaults for empty input', () => {
25
- const result = schema.parse({});
26
- expect(result).toEqual({ limit: 20, offset: 0 });
27
- });
28
-
29
- test('accepts explicit values', () => {
30
- const result = schema.parse({ cursor: 'abc', limit: 50, offset: 10 });
31
- expect(result).toEqual({ cursor: 'abc', limit: 50, offset: 10 });
32
- });
33
-
34
- test('rejects non-number limit', () => {
35
- expect(() => schema.parse({ limit: 'ten' })).toThrow();
36
- });
37
-
38
- test('rejects non-string cursor', () => {
39
- expect(() => schema.parse({ cursor: 123 })).toThrow();
40
- });
41
- });
42
-
43
- // ---------------------------------------------------------------------------
44
- // paginatedOutput
45
- // ---------------------------------------------------------------------------
46
-
47
- describe('paginatedOutput', () => {
48
- const schema = paginatedOutput(z.string());
49
-
50
- test('parses valid paginated data', () => {
51
- const data = {
52
- hasMore: true,
53
- items: ['a', 'b'],
54
- nextCursor: 'x',
55
- total: 10,
56
- };
57
- expect(schema.parse(data)).toEqual(data);
58
- });
59
-
60
- test('allows missing nextCursor', () => {
61
- const data = { hasMore: false, items: [], total: 0 };
62
- expect(schema.parse(data)).toEqual(data);
63
- });
64
-
65
- test('rejects wrong item type', () => {
66
- expect(() =>
67
- schema.parse({ hasMore: false, items: [1], total: 1 })
68
- ).toThrow();
69
- });
70
-
71
- test('rejects missing total', () => {
72
- expect(() => schema.parse({ hasMore: false, items: [] })).toThrow();
73
- });
74
- });
75
-
76
- // ---------------------------------------------------------------------------
77
- // bulkOutput
78
- // ---------------------------------------------------------------------------
79
-
80
- describe('bulkOutput', () => {
81
- const schema = bulkOutput(z.object({ id: z.number() }));
82
-
83
- test('parses valid bulk result', () => {
84
- const data = {
85
- failed: 0,
86
- items: [{ id: 1 }, { id: 2 }],
87
- succeeded: 2,
88
- };
89
- expect(schema.parse(data)).toEqual(data);
90
- });
91
-
92
- test('parses bulk result with errors', () => {
93
- const data = {
94
- errors: [{ index: 1, message: 'duplicate' }],
95
- failed: 1,
96
- items: [{ id: 1 }],
97
- succeeded: 1,
98
- };
99
- expect(schema.parse(data)).toEqual(data);
100
- });
101
-
102
- test('rejects missing succeeded field', () => {
103
- expect(() => schema.parse({ failed: 0, items: [] })).toThrow();
104
- });
105
-
106
- test('rejects invalid error shape', () => {
107
- expect(() =>
108
- schema.parse({
109
- errors: [{ bad: true }],
110
- failed: 0,
111
- items: [],
112
- succeeded: 0,
113
- })
114
- ).toThrow();
115
- });
116
- });
117
-
118
- // ---------------------------------------------------------------------------
119
- // timestampFields
120
- // ---------------------------------------------------------------------------
121
-
122
- describe('timestampFields', () => {
123
- const schema = timestampFields();
124
-
125
- test('parses valid timestamps', () => {
126
- const data = {
127
- createdAt: '2024-01-01T00:00:00Z',
128
- updatedAt: '2024-06-01T00:00:00Z',
129
- };
130
- expect(schema.parse(data)).toEqual(data);
131
- });
132
-
133
- test('rejects missing createdAt', () => {
134
- expect(() => schema.parse({ updatedAt: '2024-01-01' })).toThrow();
135
- });
136
-
137
- test('rejects non-string values', () => {
138
- expect(() => schema.parse({ createdAt: 123, updatedAt: 456 })).toThrow();
139
- });
140
- });
141
-
142
- // ---------------------------------------------------------------------------
143
- // dateRangeFields
144
- // ---------------------------------------------------------------------------
145
-
146
- describe('dateRangeFields', () => {
147
- const schema = dateRangeFields();
148
-
149
- test('parses empty object (both optional)', () => {
150
- expect(schema.parse({})).toEqual({});
151
- });
152
-
153
- test('parses with both fields', () => {
154
- const data = { since: '2024-01-01', until: '2024-12-31' };
155
- expect(schema.parse(data)).toEqual(data);
156
- });
157
-
158
- test('parses with only since', () => {
159
- expect(schema.parse({ since: '2024-01-01' })).toEqual({
160
- since: '2024-01-01',
161
- });
162
- });
163
-
164
- test('rejects non-string values', () => {
165
- expect(() => schema.parse({ since: 42 })).toThrow();
166
- });
167
- });
168
-
169
- // ---------------------------------------------------------------------------
170
- // sortFields
171
- // ---------------------------------------------------------------------------
172
-
173
- describe('sortFields', () => {
174
- const schema = sortFields(['name', 'createdAt', 'updatedAt']);
175
-
176
- test('applies default sortOrder', () => {
177
- const result = schema.parse({});
178
- expect(result).toEqual({ sortOrder: 'asc' });
179
- });
180
-
181
- test('accepts valid sortBy and sortOrder', () => {
182
- const result = schema.parse({ sortBy: 'name', sortOrder: 'desc' });
183
- expect(result).toEqual({ sortBy: 'name', sortOrder: 'desc' });
184
- });
185
-
186
- test('rejects sortBy not in allowed list', () => {
187
- expect(() => schema.parse({ sortBy: 'email' })).toThrow();
188
- });
189
-
190
- test('rejects invalid sortOrder', () => {
191
- expect(() => schema.parse({ sortOrder: 'up' })).toThrow();
192
- });
193
- });
194
-
195
- // ---------------------------------------------------------------------------
196
- // statusFields
197
- // ---------------------------------------------------------------------------
198
-
199
- describe('statusFields', () => {
200
- const schema = statusFields();
201
-
202
- test('parses each valid status', () => {
203
- for (const s of [
204
- 'pending',
205
- 'running',
206
- 'completed',
207
- 'failed',
208
- 'cancelled',
209
- ] as const) {
210
- expect(schema.parse({ status: s })).toEqual({ status: s });
211
- }
212
- });
213
-
214
- test('rejects invalid status', () => {
215
- expect(() => schema.parse({ status: 'unknown' })).toThrow();
216
- });
217
-
218
- test('rejects missing status', () => {
219
- expect(() => schema.parse({})).toThrow();
220
- });
221
- });
222
-
223
- // ---------------------------------------------------------------------------
224
- // changeOutput
225
- // ---------------------------------------------------------------------------
226
-
227
- describe('changeOutput', () => {
228
- const schema = changeOutput(z.object({ name: z.string() }));
229
-
230
- test('parses with both before and after', () => {
231
- const data = { after: { name: 'new' }, before: { name: 'old' } };
232
- expect(schema.parse(data)).toEqual(data);
233
- });
234
-
235
- test('parses with only after (before is optional)', () => {
236
- const data = { after: { name: 'created' } };
237
- expect(schema.parse(data)).toEqual(data);
238
- });
239
-
240
- test('rejects missing after', () => {
241
- expect(() => schema.parse({ before: { name: 'old' } })).toThrow();
242
- });
243
-
244
- test('rejects invalid schema shape', () => {
245
- expect(() => schema.parse({ after: { name: 123 } })).toThrow();
246
- });
247
- });
248
-
249
- // ---------------------------------------------------------------------------
250
- // progressFields
251
- // ---------------------------------------------------------------------------
252
-
253
- describe('progressFields', () => {
254
- const schema = progressFields();
255
-
256
- test('parses valid progress', () => {
257
- const data = { current: 5, percentage: 50, total: 10 };
258
- expect(schema.parse(data)).toEqual(data);
259
- });
260
-
261
- test('allows missing percentage', () => {
262
- const data = { current: 3, total: 10 };
263
- expect(schema.parse(data)).toEqual(data);
264
- });
265
-
266
- test('rejects missing current', () => {
267
- expect(() => schema.parse({ total: 10 })).toThrow();
268
- });
269
-
270
- test('rejects non-number values', () => {
271
- expect(() => schema.parse({ current: 'five', total: 10 })).toThrow();
272
- });
273
- });