@rayselfs/cf-rule-engine 1.5.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 (228) hide show
  1. package/README.md +276 -0
  2. package/dist/adapters/cf-function.cjs +10 -0
  3. package/dist/adapters/cf-function.d.cts +2 -0
  4. package/dist/adapters/cf-function.d.ts +2 -0
  5. package/dist/adapters/cf-function.js +10 -0
  6. package/dist/adapters/lambda-edge.cjs +10 -0
  7. package/dist/adapters/lambda-edge.d.cts +2 -0
  8. package/dist/adapters/lambda-edge.d.ts +2 -0
  9. package/dist/adapters/lambda-edge.js +10 -0
  10. package/dist/behaviors/construct-response.cjs +7 -0
  11. package/dist/behaviors/construct-response.d.cts +62 -0
  12. package/dist/behaviors/construct-response.d.ts +62 -0
  13. package/dist/behaviors/construct-response.js +7 -0
  14. package/dist/behaviors/copy-header.cjs +7 -0
  15. package/dist/behaviors/copy-header.d.cts +27 -0
  16. package/dist/behaviors/copy-header.d.ts +27 -0
  17. package/dist/behaviors/copy-header.js +7 -0
  18. package/dist/behaviors/directory-index.cjs +7 -0
  19. package/dist/behaviors/directory-index.d.cts +33 -0
  20. package/dist/behaviors/directory-index.d.ts +33 -0
  21. package/dist/behaviors/directory-index.js +7 -0
  22. package/dist/behaviors/image-optimize.cjs +9 -0
  23. package/dist/behaviors/image-optimize.d.cts +103 -0
  24. package/dist/behaviors/image-optimize.d.ts +103 -0
  25. package/dist/behaviors/image-optimize.js +9 -0
  26. package/dist/behaviors/index.cjs +126 -0
  27. package/dist/behaviors/index.d.cts +46 -0
  28. package/dist/behaviors/index.d.ts +46 -0
  29. package/dist/behaviors/index.js +126 -0
  30. package/dist/behaviors/redirect.cjs +7 -0
  31. package/dist/behaviors/redirect.d.cts +44 -0
  32. package/dist/behaviors/redirect.d.ts +44 -0
  33. package/dist/behaviors/redirect.js +7 -0
  34. package/dist/behaviors/remove-response-headers.cjs +7 -0
  35. package/dist/behaviors/remove-response-headers.d.cts +27 -0
  36. package/dist/behaviors/remove-response-headers.d.ts +27 -0
  37. package/dist/behaviors/remove-response-headers.js +7 -0
  38. package/dist/behaviors/rewrite-uri.cjs +7 -0
  39. package/dist/behaviors/rewrite-uri.d.cts +43 -0
  40. package/dist/behaviors/rewrite-uri.d.ts +43 -0
  41. package/dist/behaviors/rewrite-uri.js +7 -0
  42. package/dist/behaviors/set-cache-control.cjs +7 -0
  43. package/dist/behaviors/set-cache-control.d.cts +29 -0
  44. package/dist/behaviors/set-cache-control.d.ts +29 -0
  45. package/dist/behaviors/set-cache-control.js +7 -0
  46. package/dist/behaviors/set-cors-headers.cjs +7 -0
  47. package/dist/behaviors/set-cors-headers.d.cts +76 -0
  48. package/dist/behaviors/set-cors-headers.d.ts +76 -0
  49. package/dist/behaviors/set-cors-headers.js +7 -0
  50. package/dist/behaviors/set-csp.cjs +7 -0
  51. package/dist/behaviors/set-csp.d.cts +48 -0
  52. package/dist/behaviors/set-csp.d.ts +48 -0
  53. package/dist/behaviors/set-csp.js +7 -0
  54. package/dist/behaviors/set-request-header.cjs +7 -0
  55. package/dist/behaviors/set-request-header.d.cts +26 -0
  56. package/dist/behaviors/set-request-header.d.ts +26 -0
  57. package/dist/behaviors/set-request-header.js +7 -0
  58. package/dist/behaviors/set-response-header.cjs +7 -0
  59. package/dist/behaviors/set-response-header.d.cts +28 -0
  60. package/dist/behaviors/set-response-header.d.ts +28 -0
  61. package/dist/behaviors/set-response-header.js +7 -0
  62. package/dist/behaviors/set-security-headers.cjs +7 -0
  63. package/dist/behaviors/set-security-headers.d.cts +55 -0
  64. package/dist/behaviors/set-security-headers.d.ts +55 -0
  65. package/dist/behaviors/set-security-headers.js +7 -0
  66. package/dist/behaviors/strip-query-params.cjs +7 -0
  67. package/dist/behaviors/strip-query-params.d.cts +25 -0
  68. package/dist/behaviors/strip-query-params.d.ts +25 -0
  69. package/dist/behaviors/strip-query-params.js +7 -0
  70. package/dist/cf-function-D27hUVtN.d.cts +70 -0
  71. package/dist/cf-function-u0PvbW_s.d.ts +70 -0
  72. package/dist/chunk-2DE6WPPL.js +21 -0
  73. package/dist/chunk-32SMWYAF.cjs +13 -0
  74. package/dist/chunk-3BBLG4IX.cjs +132 -0
  75. package/dist/chunk-3PVDUC5M.js +12 -0
  76. package/dist/chunk-5CPBXZ4X.js +12 -0
  77. package/dist/chunk-5PT5X62W.js +98 -0
  78. package/dist/chunk-63WIEBQB.cjs +50 -0
  79. package/dist/chunk-6DBZBV2M.js +42 -0
  80. package/dist/chunk-75ZPJI57.cjs +9 -0
  81. package/dist/chunk-B4WEJSEZ.cjs +14 -0
  82. package/dist/chunk-BDNPQ7AU.js +21 -0
  83. package/dist/chunk-BUAIBB3N.js +14 -0
  84. package/dist/chunk-BZQJYOU2.js +12 -0
  85. package/dist/chunk-C32DL3EP.js +13 -0
  86. package/dist/chunk-CF5PWWTF.cjs +15 -0
  87. package/dist/chunk-CV234DQT.cjs +14 -0
  88. package/dist/chunk-D47P7HVZ.cjs +12 -0
  89. package/dist/chunk-DSSFFJWL.js +38 -0
  90. package/dist/chunk-FTP7NLKX.js +29 -0
  91. package/dist/chunk-G7JGTBTT.cjs +8 -0
  92. package/dist/chunk-GK5JX7OM.cjs +39 -0
  93. package/dist/chunk-H2LO6MQG.js +50 -0
  94. package/dist/chunk-IBXAK2A4.cjs +21 -0
  95. package/dist/chunk-JGJW7D2N.cjs +12 -0
  96. package/dist/chunk-JU5WX5RU.cjs +21 -0
  97. package/dist/chunk-KW5YBTSD.js +12 -0
  98. package/dist/chunk-KZ72PI2A.js +0 -0
  99. package/dist/chunk-L7NBJ4JA.cjs +12 -0
  100. package/dist/chunk-LBJUCJF2.js +14 -0
  101. package/dist/chunk-LNQPYKGG.js +20 -0
  102. package/dist/chunk-LO2BO3RU.js +15 -0
  103. package/dist/chunk-LSCC62CZ.cjs +13 -0
  104. package/dist/chunk-LTLBEBKL.cjs +42 -0
  105. package/dist/chunk-M5KUQBDW.js +17 -0
  106. package/dist/chunk-MEHWLQLR.cjs +1 -0
  107. package/dist/chunk-MLKGABMK.js +9 -0
  108. package/dist/chunk-MRPTC74I.cjs +29 -0
  109. package/dist/chunk-MSES76XK.cjs +14 -0
  110. package/dist/chunk-MVGYPBYB.cjs +16 -0
  111. package/dist/chunk-NPMGSPNL.js +88 -0
  112. package/dist/chunk-OSGZTNTS.cjs +42 -0
  113. package/dist/chunk-OSZWDCTS.cjs +12 -0
  114. package/dist/chunk-OTFDML3K.cjs +11 -0
  115. package/dist/chunk-PPUHEL4H.cjs +17 -0
  116. package/dist/chunk-PY3JMRDG.js +11 -0
  117. package/dist/chunk-Q4NP4C3B.js +47 -0
  118. package/dist/chunk-R7WXS7BI.js +42 -0
  119. package/dist/chunk-RBBKFG5J.js +14 -0
  120. package/dist/chunk-S2AAATFN.js +16 -0
  121. package/dist/chunk-SAXDN5NS.cjs +88 -0
  122. package/dist/chunk-SGEBNQR2.cjs +14 -0
  123. package/dist/chunk-SOBTD7AD.js +39 -0
  124. package/dist/chunk-SRQF5UEJ.js +13 -0
  125. package/dist/chunk-TQLJIT4H.cjs +98 -0
  126. package/dist/chunk-U54FZCOH.cjs +14 -0
  127. package/dist/chunk-UD456E4I.js +8 -0
  128. package/dist/chunk-UI6LKDJI.cjs +19 -0
  129. package/dist/chunk-UKB5JAX4.js +32 -0
  130. package/dist/chunk-VEEOQ7TS.cjs +8 -0
  131. package/dist/chunk-VQCRSBWL.js +19 -0
  132. package/dist/chunk-WEBU4R5C.js +132 -0
  133. package/dist/chunk-WKYMSRCD.cjs +47 -0
  134. package/dist/chunk-WW7YVRAI.cjs +32 -0
  135. package/dist/chunk-WWSRNCUP.cjs +38 -0
  136. package/dist/chunk-XLSZ5RB7.js +8 -0
  137. package/dist/chunk-XPQG5IML.js +14 -0
  138. package/dist/chunk-XUI4Y22M.js +20 -0
  139. package/dist/chunk-YVUR35RN.cjs +20 -0
  140. package/dist/chunk-ZTMSH34E.js +14 -0
  141. package/dist/chunk-ZXS23HXA.cjs +20 -0
  142. package/dist/core/rule.cjs +17 -0
  143. package/dist/core/rule.d.cts +101 -0
  144. package/dist/core/rule.d.ts +101 -0
  145. package/dist/core/rule.js +17 -0
  146. package/dist/core/types.cjs +1 -0
  147. package/dist/core/types.d.cts +53 -0
  148. package/dist/core/types.d.ts +53 -0
  149. package/dist/core/types.js +1 -0
  150. package/dist/criteria/country-is.cjs +7 -0
  151. package/dist/criteria/country-is.d.cts +35 -0
  152. package/dist/criteria/country-is.d.ts +35 -0
  153. package/dist/criteria/country-is.js +7 -0
  154. package/dist/criteria/file-extension.cjs +7 -0
  155. package/dist/criteria/file-extension.d.cts +32 -0
  156. package/dist/criteria/file-extension.d.ts +32 -0
  157. package/dist/criteria/file-extension.js +7 -0
  158. package/dist/criteria/header-contains.cjs +7 -0
  159. package/dist/criteria/header-contains.d.cts +33 -0
  160. package/dist/criteria/header-contains.d.ts +33 -0
  161. package/dist/criteria/header-contains.js +7 -0
  162. package/dist/criteria/header-equals.cjs +7 -0
  163. package/dist/criteria/header-equals.d.cts +31 -0
  164. package/dist/criteria/header-equals.d.ts +31 -0
  165. package/dist/criteria/header-equals.js +7 -0
  166. package/dist/criteria/hostname-is.cjs +7 -0
  167. package/dist/criteria/hostname-is.d.cts +22 -0
  168. package/dist/criteria/hostname-is.d.ts +22 -0
  169. package/dist/criteria/hostname-is.js +7 -0
  170. package/dist/criteria/index.cjs +49 -0
  171. package/dist/criteria/index.d.cts +12 -0
  172. package/dist/criteria/index.d.ts +12 -0
  173. package/dist/criteria/index.js +49 -0
  174. package/dist/criteria/ip-cidr.cjs +8 -0
  175. package/dist/criteria/ip-cidr.d.cts +31 -0
  176. package/dist/criteria/ip-cidr.d.ts +31 -0
  177. package/dist/criteria/ip-cidr.js +8 -0
  178. package/dist/criteria/method-is.cjs +7 -0
  179. package/dist/criteria/method-is.d.cts +29 -0
  180. package/dist/criteria/method-is.d.ts +29 -0
  181. package/dist/criteria/method-is.js +7 -0
  182. package/dist/criteria/path-equals.cjs +7 -0
  183. package/dist/criteria/path-equals.d.cts +22 -0
  184. package/dist/criteria/path-equals.d.ts +22 -0
  185. package/dist/criteria/path-equals.js +7 -0
  186. package/dist/criteria/path-matches.cjs +8 -0
  187. package/dist/criteria/path-matches.d.cts +22 -0
  188. package/dist/criteria/path-matches.d.ts +22 -0
  189. package/dist/criteria/path-matches.js +8 -0
  190. package/dist/criteria/path-prefix.cjs +7 -0
  191. package/dist/criteria/path-prefix.d.cts +21 -0
  192. package/dist/criteria/path-prefix.d.ts +21 -0
  193. package/dist/criteria/path-prefix.js +7 -0
  194. package/dist/criteria/user-agent-matches.cjs +8 -0
  195. package/dist/criteria/user-agent-matches.d.cts +32 -0
  196. package/dist/criteria/user-agent-matches.d.ts +32 -0
  197. package/dist/criteria/user-agent-matches.js +8 -0
  198. package/dist/helpers/index.cjs +42 -0
  199. package/dist/helpers/index.d.cts +34 -0
  200. package/dist/helpers/index.d.ts +34 -0
  201. package/dist/helpers/index.js +42 -0
  202. package/dist/helpers/preflight-request.cjs +8 -0
  203. package/dist/helpers/preflight-request.d.cts +38 -0
  204. package/dist/helpers/preflight-request.d.ts +38 -0
  205. package/dist/helpers/preflight-request.js +8 -0
  206. package/dist/helpers/send-country-code.cjs +8 -0
  207. package/dist/helpers/send-country-code.d.cts +35 -0
  208. package/dist/helpers/send-country-code.d.ts +35 -0
  209. package/dist/helpers/send-country-code.js +8 -0
  210. package/dist/helpers/staging-whitelist.cjs +14 -0
  211. package/dist/helpers/staging-whitelist.d.cts +73 -0
  212. package/dist/helpers/staging-whitelist.d.ts +73 -0
  213. package/dist/helpers/staging-whitelist.js +14 -0
  214. package/dist/index.cjs +26 -0
  215. package/dist/index.d.cts +4 -0
  216. package/dist/index.d.ts +4 -0
  217. package/dist/index.js +26 -0
  218. package/dist/lambda-edge-D15Nf__n.d.ts +66 -0
  219. package/dist/lambda-edge-DxTOa2cg.d.cts +66 -0
  220. package/dist/shared/cidr.cjs +11 -0
  221. package/dist/shared/cidr.d.cts +8 -0
  222. package/dist/shared/cidr.d.ts +8 -0
  223. package/dist/shared/cidr.js +11 -0
  224. package/dist/shared/wildcard.cjs +11 -0
  225. package/dist/shared/wildcard.d.cts +8 -0
  226. package/dist/shared/wildcard.d.ts +8 -0
  227. package/dist/shared/wildcard.js +11 -0
  228. package/package.json +65 -0
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # @rayselfs/cf-rule-engine
2
+
3
+ A composable, tree-shakeable rule engine for **AWS CloudFront Functions and Lambda@Edge**.
4
+
5
+ Define edge rules — redirects, CORS headers, IP allowlists, token auth, image optimization — as plain TypeScript using an Akamai-inspired criteria + behaviors API. Rules are fully tree-shakeable and compile down to CloudFront-compatible JS.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @rayselfs/cf-rule-engine
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ **viewer-request** — IP blocking and method handling:
16
+
17
+ ```typescript
18
+ import { rule, not } from '@rayselfs/cf-rule-engine'
19
+ import { ipCidr, methodIs } from '@rayselfs/cf-rule-engine/criteria/index'
20
+ import { redirect, constructResponse } from '@rayselfs/cf-rule-engine/behaviors/index'
21
+ import { defineViewerRequest } from '@rayselfs/cf-rule-engine/adapters/cf-function'
22
+
23
+ export default defineViewerRequest([
24
+ rule(not(ipCidr(['10.0.0.0/8', '172.16.0.0/12'])), redirect(302, '/blocked')),
25
+ rule(methodIs(['OPTIONS']), constructResponse({ statusCode: 200, body: 'ok' })),
26
+ ])
27
+ ```
28
+
29
+ **viewer-response** — security and CORS headers:
30
+
31
+ ```typescript
32
+ import { setSecurityHeaders, setCorsHeaders } from '@rayselfs/cf-rule-engine/behaviors/index'
33
+ import { defineViewerResponse } from '@rayselfs/cf-rule-engine/adapters/cf-function'
34
+
35
+ export default defineViewerResponse([
36
+ setSecurityHeaders(),
37
+ setCorsHeaders({ allowedOrigins: ['https://www.viverse.com'] }),
38
+ ])
39
+ ```
40
+
41
+ Build and deploy:
42
+
43
+ ```bash
44
+ esbuild viewer-request.ts \
45
+ --bundle --minify --target=es2017 \
46
+ --supported:for-of=false --supported:template-literal=false --supported:arrow=false \
47
+ --format=iife --global-name=handler \
48
+ --outfile=dist/viewer-request.js
49
+ # Append handler unwrap for IIFE compatibility:
50
+ echo 'handler=handler.default||handler;' >> dist/viewer-request.js
51
+ ```
52
+
53
+ ## Concepts
54
+
55
+ Rules are composed of an optional **criteria** guard and a **behavior**. If criteria is omitted, the behavior always runs.
56
+
57
+ ```typescript
58
+ rule(criteria?, behavior) // with or without criteria guard
59
+
60
+ all([criteriaA, criteriaB]) // AND
61
+ any([criteriaA, criteriaB]) // OR
62
+ not(criteria) // NOT
63
+
64
+ chain([behaviorA, behaviorB]) // sequential: B sees mutations made by A
65
+ ```
66
+
67
+ Use `chain()` when one behavior must see the request mutations (URI rewrite, header change) made by a previous behavior. Without `chain()`, each separate `rule()` re-evaluates criteria against the **original** unmodified request.
68
+
69
+ **Adapters** normalize CloudFront's event format so the same rule definitions work across both runtimes:
70
+
71
+ | Adapter | Import | Use for |
72
+ |---|---|---|
73
+ | CF Function | `@rayselfs/cf-rule-engine/adapters/cf-function` | `viewer-request`, `viewer-response` |
74
+ | Lambda@Edge | `@rayselfs/cf-rule-engine/adapters/lambda-edge` | `viewer-request`, `viewer-response` |
75
+
76
+ ## Akamai → CloudFront Mapping
77
+
78
+ ### Criteria
79
+
80
+ | Function | Description |
81
+ |---|---|
82
+ | `pathPrefix(prefixes)` | URI starts with any prefix in the array |
83
+ | `pathEquals(paths)` | URI exactly matches any path in the array |
84
+ | `pathMatches(patterns)` | URI matches any wildcard pattern (`*`, `?`) in the array |
85
+ | `hostnameIs(hosts)` | Host header matches any host in the array |
86
+ | `methodIs(methods)` | HTTP method matches any method in the array |
87
+ | `fileExtension(exts)` | URI file extension matches any extension in the array |
88
+ | `headerEquals(name, values)` | Request header equals any value in the array |
89
+ | `headerContains(name, substrings)` | Request header contains any of the substrings (`string[]`) |
90
+ | `ipCidr(cidrs)` | Client IP is within any CIDR range in the array |
91
+ | `countryIs(codes)` | `CloudFront-Viewer-Country` matches any ISO code in the array |
92
+ | `userAgentMatches(patterns)` | User-Agent matches any wildcard pattern in the array |
93
+
94
+ ### Behaviors (`@rayselfs/cf-rule-engine/behaviors/index`)
95
+
96
+ | Function | CF Function | Lambda@Edge |
97
+ |---|---|---|
98
+ | `redirect(status, url)` | ✅ | ✅ |
99
+ | `rewriteUri(mode, value)` | ✅ | ✅ |
100
+ | `constructResponse(options)` | ✅ | ✅ |
101
+ | `setRequestHeader(name, value)` | ✅ | ✅ |
102
+ | `copyHeader(from, to)` | ✅ | ✅ |
103
+ | `setResponseHeader(name, value)` | ✅ | ✅ |
104
+ | `removeResponseHeaders(names)` | ✅ | ✅ |
105
+ | `setCorsHeaders(options)` | ✅ | ✅ |
106
+ | `stripQueryParams(params)` | ✅ | ✅ |
107
+ | `setSecurityHeaders(options)` | ✅ | ✅ |
108
+ | `setCacheControl(options)` | ✅ | ✅ |
109
+ | `setCsp(options)` | ✅ | ✅ |
110
+ | `directoryIndex(file)` | ✅ | ✅ |
111
+ | `imageOptimize(options)` | ✅ | ✅ |
112
+ | `verifyToken(options)` | ❌ | ✅ |
113
+
114
+ ## Helpers (`@rayselfs/cf-rule-engine/helpers/index`)
115
+
116
+ Helpers are pre-configured rule factories that combine multiple criteria and behaviors for common use cases.
117
+
118
+ ### sendCountryCode
119
+
120
+ Copies the `CloudFront-Viewer-Country` header to a custom request header (default: `x-viewer-country`), making the viewer's country code available to the origin server.
121
+
122
+ ```typescript
123
+ import { sendCountryCode } from '@rayselfs/cf-rule-engine/helpers/index'
124
+
125
+ rule(sendCountryCode()) // copies to x-viewer-country
126
+ rule(sendCountryCode('x-custom-country')) // copies to a custom header
127
+ ```
128
+
129
+ ### stagingIndicator
130
+
131
+ Adds `x-cf-distribution: staging` to the response when the request carries `aws-cf-cd-staging: true`. Use in `viewer-response` configs shared between the primary and staging distributions so clients can confirm via DevTools or curl which distribution served the request.
132
+
133
+ ```typescript
134
+ import { stagingIndicator } from '@rayselfs/cf-rule-engine/helpers/index'
135
+
136
+ defineViewerResponse([
137
+ setCorsHeaders({ allowedOrigins: ['https://www.viverse.com'] }),
138
+ stagingIndicator(),
139
+ ])
140
+ ```
141
+
142
+ Primary distribution requests do not carry `aws-cf-cd-staging`, so the rule is a no-op there.
143
+
144
+ ### stagingWhitelist
145
+
146
+ Restricts access to a staging environment by IP CIDR range and/or User-Agent pattern. Requests that don't match any allowed CIDR or User-Agent (and aren't on a bypassed path) are redirected with HTTP 302.
147
+
148
+ No default allowlists are included — callers must supply all CIDRs and User-Agent patterns explicitly.
149
+
150
+ ```typescript
151
+ import { stagingWhitelist } from '@rayselfs/cf-rule-engine/helpers/index'
152
+
153
+ stagingWhitelist({
154
+ cidrs: ['203.0.113.0/24', '10.0.0.0/8'],
155
+ userAgents: ['*InternalBot*', '*Prerender*'],
156
+ redirectUrl: 'https://www.example.com',
157
+ })
158
+
159
+ // With bypass paths:
160
+ stagingWhitelist({
161
+ cidrs: ['203.0.113.0/24'],
162
+ redirectUrl: 'https://www.example.com',
163
+ bypassPaths: ['/api/health', '/robots.txt'],
164
+ })
165
+ ```
166
+
167
+ **Parameters:**
168
+ - `cidrs` (required): CIDR ranges to allow (e.g. office IPs, VPN, stage VPCs)
169
+ - `userAgents`: User-Agent wildcard patterns to allow (supports `*` and `?`)
170
+ - `redirectUrl` (required): Where to redirect blocked requests
171
+ - `bypassPaths`: Paths exempt from whitelist checks (supports wildcards)
172
+
173
+ ## CF Function vs Lambda@Edge
174
+
175
+ | | CF Function | Lambda@Edge |
176
+ |---|---|---|
177
+ | Bundle size limit | **10 KB** | 1 MB (viewer), 50 MB (origin) |
178
+ | Runtime | ES 5.1 + select ES6–12 (see AWS docs) | Node.js 20.x |
179
+ | Cold start | ~1 ms | ~100 ms |
180
+ | Max execution time | 1 ms | 5 s (viewer) |
181
+ | Environment variables | ❌ | ✅ (origin events only) |
182
+ | Node.js `crypto` | ❌ | ✅ |
183
+
184
+ **Use CF Function** for: redirects, header manipulation, CORS, rewrites, IP allowlists.
185
+
186
+ **Use Lambda@Edge** for: HMAC token validation (`verifyToken`), any behavior requiring Node.js APIs.
187
+
188
+ ## Bundle Size
189
+
190
+ CF Functions have a **10 KB post-minification limit**. cf-engine is fully tree-shakeable — only imported behaviors and criteria enter the bundle.
191
+
192
+ Rough estimates per rule type:
193
+ - Base overhead ≈ 2 KB
194
+ - Redirect rule ≈ 150–200 bytes
195
+ - CIDR check ≈ 50 bytes
196
+
197
+ If a bundle exceeds ~8 KB, split heavy rule groups into a Lambda@Edge and route via CloudFront path-pattern behaviors.
198
+
199
+ ## Build
200
+
201
+ **CF Function:**
202
+
203
+ ```bash
204
+ esbuild viewer-request.ts \
205
+ --bundle --minify --target=es2017 \
206
+ --supported:for-of=false --supported:template-literal=false --supported:arrow=false \
207
+ --format=iife --global-name=handler \
208
+ --outfile=dist/viewer-request.js
209
+ # Append handler unwrap for IIFE compatibility:
210
+ echo 'handler=handler.default||handler;' >> dist/viewer-request.js
211
+ ```
212
+
213
+ **Lambda@Edge:**
214
+
215
+ ```bash
216
+ esbuild lambda-viewer-request.ts \
217
+ --bundle --minify --platform=node --target=node20 \
218
+ --format=cjs \
219
+ --outfile=dist/lambda-viewer-request.js
220
+ ```
221
+
222
+ > `dist/` must be committed in consumer repos — Terraform reads built files at `plan` time and cannot invoke a build step.
223
+
224
+ ## CF JS 2.0 Compatibility
225
+
226
+ > **v1.1.0 Breaking Change**: All criteria and combinator functions now accept arrays instead of variadic arguments.
227
+ > `ipCidr('a', 'b')` → `ipCidr(['a', 'b'])`, `all(fn1, fn2)` → `all([fn1, fn2])`.
228
+
229
+ cf-engine source code is written to be directly compatible with the CloudFront JS 2.0 runtime. The runtime is documented as ES 5.1 compliant with select ES6–12 features, but in practice the parser rejects several ES6+ syntax patterns inside esbuild-minified IIFE bundles — even some that the official docs claim are supported.
230
+
231
+ ### Build flags (required for CF Function targets)
232
+
233
+ ```bash
234
+ esbuild --target=es2017 \
235
+ --supported:for-of=false \
236
+ --supported:template-literal=false \
237
+ --supported:arrow=false \
238
+ --format=iife --global-name=handler
239
+ # Then append: echo 'handler=handler.default||handler;' >> output.js
240
+ ```
241
+
242
+ The handler unwrap is needed because esbuild IIFE wraps the export as `{default: fn}`, but CF expects a bare `handler` function.
243
+
244
+ ### Syntax avoided in cf-engine source
245
+
246
+ These patterns are NOT used anywhere in cf-engine, so esbuild cannot emit them regardless of flags:
247
+
248
+ | Pattern | Why avoided |
249
+ |---------|------------|
250
+ | `for...of` | Not in CF JS 2.0 statement list |
251
+ | Object/array spread `{...x}` `[...x]` | Not documented as supported |
252
+ | Rest parameters `...args` | Fails in minified IIFE bundles |
253
+ | Destructuring `[a, b] = arr` | Fails in minified IIFE bundles |
254
+ | Default parameters `f(x = 1)` | Not documented as supported |
255
+ | `new Map` / `new Set` | Not documented as supported |
256
+
257
+ ### Syntax handled by esbuild flags
258
+
259
+ | Pattern | esbuild flag | What esbuild does |
260
+ |---------|-------------|-------------------|
261
+ | Arrow functions `=>` | `--supported:arrow=false` | Converts to `function` |
262
+ | Template literals `` ` `` | `--supported:template-literal=false` | Converts to string concat |
263
+ | `for...of` | `--supported:for-of=false` | Converts to index loop |
264
+ | Optional chaining `?.` | `--target=es2017` | Auto-downleveled |
265
+ | Nullish coalescing `??` | `--target=es2017` | Auto-downleveled |
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ npm run build # tsup
271
+ npm run typecheck # tsc --noEmit
272
+ npm test # vitest
273
+ ```
274
+
275
+ See `samples/` for complete working examples.
276
+
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+ var _chunkSAXDN5NScjs = require('../chunk-SAXDN5NS.cjs');
5
+ require('../chunk-WKYMSRCD.cjs');
6
+ require('../chunk-75ZPJI57.cjs');
7
+
8
+
9
+
10
+ exports.defineViewerRequest = _chunkSAXDN5NScjs.defineViewerRequest; exports.defineViewerResponse = _chunkSAXDN5NScjs.defineViewerResponse;
@@ -0,0 +1,2 @@
1
+ import '../core/types.cjs';
2
+ export { d as defineViewerRequest, a as defineViewerResponse } from '../cf-function-D27hUVtN.cjs';
@@ -0,0 +1,2 @@
1
+ import '../core/types.js';
2
+ export { d as defineViewerRequest, a as defineViewerResponse } from '../cf-function-u0PvbW_s.js';
@@ -0,0 +1,10 @@
1
+ import {
2
+ defineViewerRequest,
3
+ defineViewerResponse
4
+ } from "../chunk-NPMGSPNL.js";
5
+ import "../chunk-Q4NP4C3B.js";
6
+ import "../chunk-MLKGABMK.js";
7
+ export {
8
+ defineViewerRequest,
9
+ defineViewerResponse
10
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+ var _chunk3BBLG4IXcjs = require('../chunk-3BBLG4IX.cjs');
5
+ require('../chunk-WKYMSRCD.cjs');
6
+ require('../chunk-75ZPJI57.cjs');
7
+
8
+
9
+
10
+ exports.defineViewerRequest = _chunk3BBLG4IXcjs.defineViewerRequest; exports.defineViewerResponse = _chunk3BBLG4IXcjs.defineViewerResponse;
@@ -0,0 +1,2 @@
1
+ import '../core/types.cjs';
2
+ export { d as defineViewerRequest, a as defineViewerResponse } from '../lambda-edge-DxTOa2cg.cjs';
@@ -0,0 +1,2 @@
1
+ import '../core/types.js';
2
+ export { d as defineViewerRequest, a as defineViewerResponse } from '../lambda-edge-D15Nf__n.js';
@@ -0,0 +1,10 @@
1
+ import {
2
+ defineViewerRequest,
3
+ defineViewerResponse
4
+ } from "../chunk-WEBU4R5C.js";
5
+ import "../chunk-Q4NP4C3B.js";
6
+ import "../chunk-MLKGABMK.js";
7
+ export {
8
+ defineViewerRequest,
9
+ defineViewerResponse
10
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+ var _chunkOSGZTNTScjs = require('../chunk-OSGZTNTS.cjs');
4
+ require('../chunk-75ZPJI57.cjs');
5
+
6
+
7
+ exports.constructResponse = _chunkOSGZTNTScjs.constructResponse;
@@ -0,0 +1,62 @@
1
+ import { BehaviorFn } from '../core/types.cjs';
2
+
3
+ /**
4
+ * Options for constructing a synthetic HTTP response at the edge.
5
+ */
6
+ interface ConstructResponseOptions {
7
+ /**
8
+ * The HTTP status code for the response (e.g. `200`, `403`, `404`).
9
+ */
10
+ statusCode: number;
11
+ /**
12
+ * Optional response body string. If omitted, an empty body is returned.
13
+ */
14
+ body?: string;
15
+ /**
16
+ * Optional `Content-Type` header value (e.g. `'application/json'`, `'text/plain'`).
17
+ * Omit to exclude the `Content-Type` header from the response.
18
+ */
19
+ contentType?: string;
20
+ /**
21
+ * Additional response headers to include, as a plain key-value map.
22
+ * Header names are automatically lowercased.
23
+ *
24
+ * @example `{ 'x-request-id': '123', 'retry-after': '60' }`
25
+ */
26
+ headers?: Record<string, string>;
27
+ }
28
+ /**
29
+ * Constructs and returns a synthetic HTTP response directly from the edge,
30
+ * without forwarding the request to the origin.
31
+ *
32
+ * The response always includes `Cache-Control: no-store`.
33
+ *
34
+ * Akamai equivalent: `constructResponse` behavior.
35
+ *
36
+ * @param options - The response configuration: status code, optional body, content type,
37
+ * and any additional headers.
38
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { constructResponse } from '@viverse/cf-engine/behaviors'
43
+ * import { methodIs, pathPrefix } from '@viverse/cf-engine/criteria'
44
+ * import { rule } from '@viverse/cf-engine'
45
+ *
46
+ * // Respond to OPTIONS preflight requests
47
+ * rule(methodIs(['OPTIONS']), constructResponse({ statusCode: 200, body: '' }))
48
+ *
49
+ * // Block with 403 and JSON error body
50
+ * rule(
51
+ * pathPrefix(['/admin/']),
52
+ * constructResponse({
53
+ * statusCode: 403,
54
+ * contentType: 'application/json',
55
+ * body: JSON.stringify({ error: 'Forbidden' }),
56
+ * }),
57
+ * )
58
+ * ```
59
+ */
60
+ declare function constructResponse(options: ConstructResponseOptions): BehaviorFn;
61
+
62
+ export { type ConstructResponseOptions, constructResponse };
@@ -0,0 +1,62 @@
1
+ import { BehaviorFn } from '../core/types.js';
2
+
3
+ /**
4
+ * Options for constructing a synthetic HTTP response at the edge.
5
+ */
6
+ interface ConstructResponseOptions {
7
+ /**
8
+ * The HTTP status code for the response (e.g. `200`, `403`, `404`).
9
+ */
10
+ statusCode: number;
11
+ /**
12
+ * Optional response body string. If omitted, an empty body is returned.
13
+ */
14
+ body?: string;
15
+ /**
16
+ * Optional `Content-Type` header value (e.g. `'application/json'`, `'text/plain'`).
17
+ * Omit to exclude the `Content-Type` header from the response.
18
+ */
19
+ contentType?: string;
20
+ /**
21
+ * Additional response headers to include, as a plain key-value map.
22
+ * Header names are automatically lowercased.
23
+ *
24
+ * @example `{ 'x-request-id': '123', 'retry-after': '60' }`
25
+ */
26
+ headers?: Record<string, string>;
27
+ }
28
+ /**
29
+ * Constructs and returns a synthetic HTTP response directly from the edge,
30
+ * without forwarding the request to the origin.
31
+ *
32
+ * The response always includes `Cache-Control: no-store`.
33
+ *
34
+ * Akamai equivalent: `constructResponse` behavior.
35
+ *
36
+ * @param options - The response configuration: status code, optional body, content type,
37
+ * and any additional headers.
38
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { constructResponse } from '@viverse/cf-engine/behaviors'
43
+ * import { methodIs, pathPrefix } from '@viverse/cf-engine/criteria'
44
+ * import { rule } from '@viverse/cf-engine'
45
+ *
46
+ * // Respond to OPTIONS preflight requests
47
+ * rule(methodIs(['OPTIONS']), constructResponse({ statusCode: 200, body: '' }))
48
+ *
49
+ * // Block with 403 and JSON error body
50
+ * rule(
51
+ * pathPrefix(['/admin/']),
52
+ * constructResponse({
53
+ * statusCode: 403,
54
+ * contentType: 'application/json',
55
+ * body: JSON.stringify({ error: 'Forbidden' }),
56
+ * }),
57
+ * )
58
+ * ```
59
+ */
60
+ declare function constructResponse(options: ConstructResponseOptions): BehaviorFn;
61
+
62
+ export { type ConstructResponseOptions, constructResponse };
@@ -0,0 +1,7 @@
1
+ import {
2
+ constructResponse
3
+ } from "../chunk-6DBZBV2M.js";
4
+ import "../chunk-MLKGABMK.js";
5
+ export {
6
+ constructResponse
7
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+ var _chunkJU5WX5RUcjs = require('../chunk-JU5WX5RU.cjs');
4
+ require('../chunk-75ZPJI57.cjs');
5
+
6
+
7
+ exports.copyHeader = _chunkJU5WX5RUcjs.copyHeader;
@@ -0,0 +1,27 @@
1
+ import { BehaviorFn } from '../core/types.cjs';
2
+
3
+ /**
4
+ * Copies the value of one request header into another request header.
5
+ *
6
+ * If the source header does not exist in the request, the behavior is a no-op —
7
+ * the request is passed through unchanged. Header names are matched and written
8
+ * in lowercase.
9
+ *
10
+ * Commonly used to forward CloudFront-injected headers (e.g. `CloudFront-Viewer-Country`)
11
+ * under a custom name that the origin server expects.
12
+ *
13
+ * @param sourceHeader - The request header to read from. Case-insensitive.
14
+ * @param targetHeader - The request header to write to. Case-insensitive.
15
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { copyHeader } from '@viverse/cf-engine/behaviors'
20
+ * import { rule } from '@viverse/cf-engine'
21
+ *
22
+ * rule(copyHeader('cloudfront-viewer-country', 'x-htc-request-country-code'))
23
+ * ```
24
+ */
25
+ declare function copyHeader(sourceHeader: string, targetHeader: string): BehaviorFn;
26
+
27
+ export { copyHeader };
@@ -0,0 +1,27 @@
1
+ import { BehaviorFn } from '../core/types.js';
2
+
3
+ /**
4
+ * Copies the value of one request header into another request header.
5
+ *
6
+ * If the source header does not exist in the request, the behavior is a no-op —
7
+ * the request is passed through unchanged. Header names are matched and written
8
+ * in lowercase.
9
+ *
10
+ * Commonly used to forward CloudFront-injected headers (e.g. `CloudFront-Viewer-Country`)
11
+ * under a custom name that the origin server expects.
12
+ *
13
+ * @param sourceHeader - The request header to read from. Case-insensitive.
14
+ * @param targetHeader - The request header to write to. Case-insensitive.
15
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { copyHeader } from '@viverse/cf-engine/behaviors'
20
+ * import { rule } from '@viverse/cf-engine'
21
+ *
22
+ * rule(copyHeader('cloudfront-viewer-country', 'x-htc-request-country-code'))
23
+ * ```
24
+ */
25
+ declare function copyHeader(sourceHeader: string, targetHeader: string): BehaviorFn;
26
+
27
+ export { copyHeader };
@@ -0,0 +1,7 @@
1
+ import {
2
+ copyHeader
3
+ } from "../chunk-BDNPQ7AU.js";
4
+ import "../chunk-MLKGABMK.js";
5
+ export {
6
+ copyHeader
7
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+ var _chunkLTLBEBKLcjs = require('../chunk-LTLBEBKL.cjs');
4
+ require('../chunk-75ZPJI57.cjs');
5
+
6
+
7
+ exports.directoryIndex = _chunkLTLBEBKLcjs.directoryIndex;
@@ -0,0 +1,33 @@
1
+ import { BehaviorFn } from '../core/types.cjs';
2
+
3
+ /**
4
+ * Handles directory index routing for static site hosting, applying three transformations:
5
+ *
6
+ * 1. **Directory request** (`/path/`) → rewrites URI to `/path/index.html` (or custom file).
7
+ * 2. **Index file request** (`/path/index.html`) → 301 redirects to `/path/`.
8
+ * 3. **Extensionless path** (`/path/about`) → 301 redirects to `/path/about/`.
9
+ *
10
+ * This mirrors the behavior of S3 static website hosting when accessed via CloudFront
11
+ * without using an S3 website endpoint.
12
+ *
13
+ * @param indexFile - The index file name to append to directory URIs.
14
+ * Default: `'index.html'`.
15
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { directoryIndex } from '@viverse/cf-engine/behaviors'
20
+ * import { rule } from '@viverse/cf-engine'
21
+ * import { defineViewerRequest } from '@viverse/cf-engine/adapters/cf-function'
22
+ *
23
+ * export default defineViewerRequest([
24
+ * rule(directoryIndex()),
25
+ * ])
26
+ *
27
+ * // With a custom index file
28
+ * rule(directoryIndex('default.html'))
29
+ * ```
30
+ */
31
+ declare function directoryIndex(indexFile?: string): BehaviorFn;
32
+
33
+ export { directoryIndex };
@@ -0,0 +1,33 @@
1
+ import { BehaviorFn } from '../core/types.js';
2
+
3
+ /**
4
+ * Handles directory index routing for static site hosting, applying three transformations:
5
+ *
6
+ * 1. **Directory request** (`/path/`) → rewrites URI to `/path/index.html` (or custom file).
7
+ * 2. **Index file request** (`/path/index.html`) → 301 redirects to `/path/`.
8
+ * 3. **Extensionless path** (`/path/about`) → 301 redirects to `/path/about/`.
9
+ *
10
+ * This mirrors the behavior of S3 static website hosting when accessed via CloudFront
11
+ * without using an S3 website endpoint.
12
+ *
13
+ * @param indexFile - The index file name to append to directory URIs.
14
+ * Default: `'index.html'`.
15
+ * @returns A `BehaviorFn` to use as the second argument to `rule()`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { directoryIndex } from '@viverse/cf-engine/behaviors'
20
+ * import { rule } from '@viverse/cf-engine'
21
+ * import { defineViewerRequest } from '@viverse/cf-engine/adapters/cf-function'
22
+ *
23
+ * export default defineViewerRequest([
24
+ * rule(directoryIndex()),
25
+ * ])
26
+ *
27
+ * // With a custom index file
28
+ * rule(directoryIndex('default.html'))
29
+ * ```
30
+ */
31
+ declare function directoryIndex(indexFile?: string): BehaviorFn;
32
+
33
+ export { directoryIndex };
@@ -0,0 +1,7 @@
1
+ import {
2
+ directoryIndex
3
+ } from "../chunk-R7WXS7BI.js";
4
+ import "../chunk-MLKGABMK.js";
5
+ export {
6
+ directoryIndex
7
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+ var _chunkTQLJIT4Hcjs = require('../chunk-TQLJIT4H.cjs');
5
+ require('../chunk-75ZPJI57.cjs');
6
+
7
+
8
+
9
+ exports.imageOptimize = _chunkTQLJIT4Hcjs.imageOptimize; exports.resolveImageParams = _chunkTQLJIT4Hcjs.resolveImageParams;