@selvajs/server 0.2.0-beta.1 → 0.2.0-beta.3

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 (192) hide show
  1. package/dist/access/__tests__/project-access-input.test.d.ts +2 -0
  2. package/dist/access/__tests__/project-access-input.test.d.ts.map +1 -0
  3. package/dist/access/__tests__/project-access-input.test.js +146 -0
  4. package/dist/access/__tests__/project-access-input.test.js.map +1 -0
  5. package/dist/access/index.d.ts +2 -0
  6. package/dist/access/index.d.ts.map +1 -0
  7. package/dist/access/index.js +4 -0
  8. package/dist/access/index.js.map +1 -0
  9. package/dist/access/project-access-input.d.ts +52 -0
  10. package/dist/access/project-access-input.d.ts.map +1 -0
  11. package/dist/access/project-access-input.js +56 -0
  12. package/dist/access/project-access-input.js.map +1 -0
  13. package/dist/compute/__tests__/client-cache.test.js +70 -1
  14. package/dist/compute/__tests__/client-cache.test.js.map +1 -1
  15. package/dist/compute/__tests__/definition-byte-cache.test.d.ts +2 -0
  16. package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +1 -0
  17. package/dist/compute/__tests__/definition-byte-cache.test.js +107 -0
  18. package/dist/compute/__tests__/definition-byte-cache.test.js.map +1 -0
  19. package/dist/compute/__tests__/rate-limit.test.js +36 -0
  20. package/dist/compute/__tests__/rate-limit.test.js.map +1 -1
  21. package/dist/compute/__tests__/solve-cache.test.d.ts +13 -0
  22. package/dist/compute/__tests__/solve-cache.test.d.ts.map +1 -0
  23. package/dist/compute/__tests__/solve-cache.test.js +234 -0
  24. package/dist/compute/__tests__/solve-cache.test.js.map +1 -0
  25. package/dist/compute/__tests__/solve-pipeline.test.js +327 -15
  26. package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -1
  27. package/dist/compute/__tests__/transform-input.test.js +7 -1
  28. package/dist/compute/__tests__/transform-input.test.js.map +1 -1
  29. package/dist/compute/client-cache.d.ts +32 -8
  30. package/dist/compute/client-cache.d.ts.map +1 -1
  31. package/dist/compute/client-cache.js +24 -6
  32. package/dist/compute/client-cache.js.map +1 -1
  33. package/dist/compute/definition-byte-cache.d.ts +85 -0
  34. package/dist/compute/definition-byte-cache.d.ts.map +1 -0
  35. package/dist/compute/definition-byte-cache.js +110 -0
  36. package/dist/compute/definition-byte-cache.js.map +1 -0
  37. package/dist/compute/index.d.ts +7 -2
  38. package/dist/compute/index.d.ts.map +1 -1
  39. package/dist/compute/index.js +6 -1
  40. package/dist/compute/index.js.map +1 -1
  41. package/dist/compute/limits.d.ts +62 -0
  42. package/dist/compute/limits.d.ts.map +1 -1
  43. package/dist/compute/limits.js +35 -1
  44. package/dist/compute/limits.js.map +1 -1
  45. package/dist/compute/memory-solve-cache.d.ts +58 -0
  46. package/dist/compute/memory-solve-cache.d.ts.map +1 -0
  47. package/dist/compute/memory-solve-cache.js +0 -0
  48. package/dist/compute/memory-solve-cache.js.map +1 -0
  49. package/dist/compute/rate-limit.d.ts +12 -0
  50. package/dist/compute/rate-limit.d.ts.map +1 -1
  51. package/dist/compute/rate-limit.js +18 -0
  52. package/dist/compute/rate-limit.js.map +1 -1
  53. package/dist/compute/solve-cache-envelope.d.ts +53 -0
  54. package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
  55. package/dist/compute/solve-cache-envelope.js +66 -0
  56. package/dist/compute/solve-cache-envelope.js.map +1 -0
  57. package/dist/compute/solve-cache-key.d.ts +65 -0
  58. package/dist/compute/solve-cache-key.d.ts.map +1 -0
  59. package/dist/compute/solve-cache-key.js +50 -0
  60. package/dist/compute/solve-cache-key.js.map +1 -0
  61. package/dist/compute/solve-cache-single-flight.d.ts +35 -0
  62. package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
  63. package/dist/compute/solve-cache-single-flight.js +48 -0
  64. package/dist/compute/solve-cache-single-flight.js.map +1 -0
  65. package/dist/compute/solve-pipeline.d.ts +71 -8
  66. package/dist/compute/solve-pipeline.d.ts.map +1 -1
  67. package/dist/compute/solve-pipeline.js +198 -15
  68. package/dist/compute/solve-pipeline.js.map +1 -1
  69. package/dist/compute/transform-input.d.ts.map +1 -1
  70. package/dist/compute/transform-input.js +21 -6
  71. package/dist/compute/transform-input.js.map +1 -1
  72. package/dist/definitions/__tests__/definition-service.test.d.ts +14 -0
  73. package/dist/definitions/__tests__/definition-service.test.d.ts.map +1 -0
  74. package/dist/definitions/__tests__/definition-service.test.js +95 -0
  75. package/dist/definitions/__tests__/definition-service.test.js.map +1 -0
  76. package/dist/definitions/definition-service.d.ts +1 -1
  77. package/dist/definitions/definition-service.d.ts.map +1 -1
  78. package/dist/definitions/definition-service.js +8 -4
  79. package/dist/definitions/definition-service.js.map +1 -1
  80. package/dist/errors/SentryErrorReporter.d.ts +17 -0
  81. package/dist/errors/SentryErrorReporter.d.ts.map +1 -0
  82. package/dist/errors/SentryErrorReporter.js +60 -0
  83. package/dist/errors/SentryErrorReporter.js.map +1 -0
  84. package/dist/errors/index.d.ts +2 -0
  85. package/dist/errors/index.d.ts.map +1 -0
  86. package/dist/errors/index.js +3 -0
  87. package/dist/errors/index.js.map +1 -0
  88. package/dist/http/__tests__/body-size.test.d.ts +2 -0
  89. package/dist/http/__tests__/body-size.test.d.ts.map +1 -0
  90. package/dist/http/__tests__/body-size.test.js +24 -0
  91. package/dist/http/__tests__/body-size.test.js.map +1 -0
  92. package/dist/http/__tests__/route-classifier.test.d.ts +2 -0
  93. package/dist/http/__tests__/route-classifier.test.d.ts.map +1 -0
  94. package/dist/http/__tests__/route-classifier.test.js +54 -0
  95. package/dist/http/__tests__/route-classifier.test.js.map +1 -0
  96. package/dist/http/__tests__/safe-redirect.test.d.ts +2 -0
  97. package/dist/http/__tests__/safe-redirect.test.d.ts.map +1 -0
  98. package/dist/http/__tests__/safe-redirect.test.js +44 -0
  99. package/dist/http/__tests__/safe-redirect.test.js.map +1 -0
  100. package/dist/http/__tests__/security-headers.test.d.ts +2 -0
  101. package/dist/http/__tests__/security-headers.test.d.ts.map +1 -0
  102. package/dist/http/__tests__/security-headers.test.js +24 -0
  103. package/dist/http/__tests__/security-headers.test.js.map +1 -0
  104. package/dist/http/body-size.d.ts +21 -0
  105. package/dist/http/body-size.d.ts.map +1 -0
  106. package/dist/http/body-size.js +20 -0
  107. package/dist/http/body-size.js.map +1 -0
  108. package/dist/http/index.d.ts +5 -0
  109. package/dist/http/index.d.ts.map +1 -0
  110. package/dist/http/index.js +6 -0
  111. package/dist/http/index.js.map +1 -0
  112. package/dist/http/redirect.d.ts +11 -0
  113. package/dist/http/redirect.d.ts.map +1 -0
  114. package/dist/http/redirect.js +20 -0
  115. package/dist/http/redirect.js.map +1 -0
  116. package/dist/http/route-classifier.d.ts +47 -0
  117. package/dist/http/route-classifier.d.ts.map +1 -0
  118. package/dist/http/route-classifier.js +37 -0
  119. package/dist/http/route-classifier.js.map +1 -0
  120. package/dist/http/security-headers.d.ts +31 -0
  121. package/dist/http/security-headers.d.ts.map +1 -0
  122. package/dist/http/security-headers.js +31 -0
  123. package/dist/http/security-headers.js.map +1 -0
  124. package/dist/index.d.ts +11 -2
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +11 -2
  127. package/dist/index.js.map +1 -1
  128. package/dist/ops/__tests__/semver.test.d.ts +2 -0
  129. package/dist/ops/__tests__/semver.test.d.ts.map +1 -0
  130. package/dist/ops/__tests__/semver.test.js +38 -0
  131. package/dist/ops/__tests__/semver.test.js.map +1 -0
  132. package/dist/ops/index.d.ts +2 -0
  133. package/dist/ops/index.d.ts.map +1 -0
  134. package/dist/ops/index.js +3 -0
  135. package/dist/ops/index.js.map +1 -0
  136. package/dist/ops/semver.d.ts +35 -0
  137. package/dist/ops/semver.d.ts.map +1 -0
  138. package/dist/ops/semver.js +55 -0
  139. package/dist/ops/semver.js.map +1 -0
  140. package/dist/tokens/__tests__/token-codec.test.d.ts +2 -0
  141. package/dist/tokens/__tests__/token-codec.test.d.ts.map +1 -0
  142. package/dist/tokens/__tests__/token-codec.test.js +50 -0
  143. package/dist/tokens/__tests__/token-codec.test.js.map +1 -0
  144. package/dist/tokens/index.d.ts +2 -0
  145. package/dist/tokens/index.d.ts.map +1 -0
  146. package/dist/tokens/index.js +3 -0
  147. package/dist/tokens/index.js.map +1 -0
  148. package/dist/tokens/token-codec.d.ts +54 -0
  149. package/dist/tokens/token-codec.d.ts.map +1 -0
  150. package/dist/tokens/token-codec.js +62 -0
  151. package/dist/tokens/token-codec.js.map +1 -0
  152. package/package.json +35 -2
  153. package/src/access/__tests__/project-access-input.test.ts +175 -0
  154. package/src/access/index.ts +9 -0
  155. package/src/access/project-access-input.ts +145 -0
  156. package/src/compute/__tests__/client-cache.test.ts +81 -1
  157. package/src/compute/__tests__/definition-byte-cache.test.ts +129 -0
  158. package/src/compute/__tests__/rate-limit.test.ts +37 -0
  159. package/src/compute/__tests__/solve-cache.test.ts +268 -0
  160. package/src/compute/__tests__/solve-pipeline.test.ts +380 -14
  161. package/src/compute/__tests__/transform-input.test.ts +8 -1
  162. package/src/compute/client-cache.ts +59 -16
  163. package/src/compute/definition-byte-cache.ts +168 -0
  164. package/src/compute/index.ts +34 -0
  165. package/src/compute/limits.ts +91 -1
  166. package/src/compute/memory-solve-cache.ts +0 -0
  167. package/src/compute/rate-limit.ts +27 -0
  168. package/src/compute/solve-cache-envelope.ts +98 -0
  169. package/src/compute/solve-cache-key.ts +85 -0
  170. package/src/compute/solve-cache-single-flight.ts +59 -0
  171. package/src/compute/solve-pipeline.ts +301 -22
  172. package/src/compute/transform-input.ts +20 -6
  173. package/src/definitions/__tests__/definition-service.test.ts +131 -0
  174. package/src/definitions/definition-service.ts +8 -4
  175. package/src/errors/SentryErrorReporter.ts +88 -0
  176. package/src/errors/index.ts +3 -0
  177. package/src/http/__tests__/body-size.test.ts +27 -0
  178. package/src/http/__tests__/route-classifier.test.ts +60 -0
  179. package/src/http/__tests__/safe-redirect.test.ts +47 -0
  180. package/src/http/__tests__/security-headers.test.ts +29 -0
  181. package/src/http/body-size.ts +24 -0
  182. package/src/http/index.ts +10 -0
  183. package/src/http/redirect.ts +16 -0
  184. package/src/http/route-classifier.ts +74 -0
  185. package/src/http/security-headers.ts +42 -0
  186. package/src/index.ts +11 -2
  187. package/src/ops/__tests__/semver.test.ts +42 -0
  188. package/src/ops/index.ts +3 -0
  189. package/src/ops/semver.ts +60 -0
  190. package/src/tokens/__tests__/token-codec.test.ts +63 -0
  191. package/src/tokens/index.ts +8 -0
  192. package/src/tokens/token-codec.ts +86 -0
@@ -0,0 +1,60 @@
1
+ export class SentryErrorReporter {
2
+ sentry;
3
+ constructor(sentry) {
4
+ this.sentry = sentry;
5
+ }
6
+ /**
7
+ * Initialize Sentry and return a reporter, or `null` if `@sentry/node` is
8
+ * not installed (optional dependency absent). Never throws — a broken
9
+ * error-tracker must not take down boot.
10
+ */
11
+ static async create(opts) {
12
+ try {
13
+ // Dynamic so the package stays optional. `@vite-ignore` keeps the
14
+ // bundler from trying to resolve it at build time when it's absent.
15
+ // `@ts-ignore` (not `@ts-expect-error`) because whether the module
16
+ // resolves at type-check time depends on the environment — it's a real
17
+ // dep in some installs, absent in others — so we can't assert the error
18
+ // is always present. The `as unknown as` cast narrows whatever loads to
19
+ // the two calls we use.
20
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
21
+ // @ts-ignore -- optional dependency, may be absent at type-check time
22
+ const sentry = (await import(/* @vite-ignore */ '@sentry/node'));
23
+ sentry.init({
24
+ dsn: opts.dsn,
25
+ environment: opts.environment,
26
+ release: opts.release,
27
+ // Error tracking only — no performance tracing by default. Flip
28
+ // this up later if you want transactions/spans.
29
+ tracesSampleRate: 0
30
+ });
31
+ return new SentryErrorReporter(sentry);
32
+ }
33
+ catch (err) {
34
+ console.error('[ErrorReporter] A Sentry DSN is set but @sentry/node could not be loaded. ' +
35
+ 'Install it (`pnpm add @sentry/node`) or unset the DSN. Falling back to no-op.', err);
36
+ return null;
37
+ }
38
+ }
39
+ capture(error, context) {
40
+ try {
41
+ const tags = { ...context?.tags };
42
+ if (context?.method)
43
+ tags.method = context.method;
44
+ if (context?.route)
45
+ tags.route = context.route;
46
+ if (context?.orgId)
47
+ tags.orgId = context.orgId;
48
+ this.sentry.captureException(error, {
49
+ tags,
50
+ user: context?.userId ? { id: context.userId } : undefined
51
+ });
52
+ }
53
+ catch (err) {
54
+ // Reporting is best-effort and sits on the error path — a second
55
+ // failure here would mask the first. Swallow after logging.
56
+ console.error('[ErrorReporter] Failed to capture error:', err);
57
+ }
58
+ }
59
+ }
60
+ //# sourceMappingURL=SentryErrorReporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SentryErrorReporter.js","sourceRoot":"","sources":["../../src/errors/SentryErrorReporter.ts"],"names":[],"mappings":"AA4BA,MAAM,OAAO,mBAAmB;IACM;IAArC,YAAqC,MAAkB;QAAlB,WAAM,GAAN,MAAM,CAAY;IAAG,CAAC;IAE3D;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAInB;QACA,IAAI,CAAC;YACJ,kEAAkE;YAClE,oEAAoE;YACpE,mEAAmE;YACnE,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,wBAAwB;YACxB,6DAA6D;YAC7D,sEAAsE;YACtE,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAA0B,CAAC;YAC1F,MAAM,CAAC,IAAI,CAAC;gBACX,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,gEAAgE;gBAChE,gDAAgD;gBAChD,gBAAgB,EAAE,CAAC;aACnB,CAAC,CAAC;YACH,OAAO,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CACZ,4EAA4E;gBAC3E,+EAA+E,EAChF,GAAG,CACH,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,OAAO,CAAC,KAAc,EAAE,OAAsB;QAC7C,IAAI,CAAC;YACJ,MAAM,IAAI,GAA2B,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,CAAC;YAC1D,IAAI,OAAO,EAAE,MAAM;gBAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAClD,IAAI,OAAO,EAAE,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;YAC/C,IAAI,OAAO,EAAE,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;YAC/C,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE;gBACnC,IAAI;gBACJ,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS;aAC1D,CAAC,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,iEAAiE;YACjE,4DAA4D;YAC5D,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,GAAG,CAAC,CAAC;QAChE,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,2 @@
1
+ export { SentryErrorReporter } from './SentryErrorReporter.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Error reporting — Sentry-backed IErrorReporter (optional @sentry/node peer).
2
+ export { SentryErrorReporter } from './SentryErrorReporter.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=body-size.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-size.test.d.ts","sourceRoot":"","sources":["../../../src/http/__tests__/body-size.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { declaredBodySizeExceeds } from '../body-size.js';
3
+ function headersWith(contentLength) {
4
+ const h = new Headers();
5
+ if (contentLength !== undefined)
6
+ h.set('content-length', contentLength);
7
+ return h;
8
+ }
9
+ describe('declaredBodySizeExceeds', () => {
10
+ it('flags a declared size over the limit', () => {
11
+ expect(declaredBodySizeExceeds(headersWith('1001'), 1000)).toBe(true);
12
+ });
13
+ it('passes a declared size at or under the limit', () => {
14
+ expect(declaredBodySizeExceeds(headersWith('1000'), 1000)).toBe(false);
15
+ expect(declaredBodySizeExceeds(headersWith('0'), 1000)).toBe(false);
16
+ });
17
+ it('passes when Content-Length is absent (chunked transfer — global limit is the backstop)', () => {
18
+ expect(declaredBodySizeExceeds(headersWith(), 1000)).toBe(false);
19
+ });
20
+ it('passes a non-numeric Content-Length rather than throwing', () => {
21
+ expect(declaredBodySizeExceeds(headersWith('not-a-number'), 1000)).toBe(false);
22
+ });
23
+ });
24
+ //# sourceMappingURL=body-size.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-size.test.js","sourceRoot":"","sources":["../../../src/http/__tests__/body-size.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,SAAS,WAAW,CAAC,aAAsB;IAC1C,MAAM,CAAC,GAAG,IAAI,OAAO,EAAE,CAAC;IACxB,IAAI,aAAa,KAAK,SAAS;QAAE,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;IACxE,OAAO,CAAC,CAAC;AACV,CAAC;AAED,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,uBAAuB,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,uBAAuB,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,CAAC,uBAAuB,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wFAAwF,EAAE,GAAG,EAAE;QACjG,MAAM,CAAC,uBAAuB,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,uBAAuB,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=route-classifier.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-classifier.test.d.ts","sourceRoot":"","sources":["../../../src/http/__tests__/route-classifier.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRouteClassifier } from '../route-classifier.js';
3
+ // Generic values — the consuming app's actual route policy is pinned by its
4
+ // own test suite; these cover the classifier mechanics.
5
+ const classifier = createRouteClassifier({
6
+ publicPages: ['/', '/login'],
7
+ publicPrefixes: ['/auth/', '/logout'],
8
+ publicApis: ['/api/health'],
9
+ selfGatingPrefix: '/api/files/',
10
+ staticPrefixes: ['/_app/', '/favicon/'],
11
+ staticPaths: ['/favicon.svg', '/robots.txt']
12
+ });
13
+ describe('createRouteClassifier', () => {
14
+ it('classifies exact pages, API allowlist, prefixes, and self-gating as public', () => {
15
+ for (const path of [
16
+ '/',
17
+ '/login',
18
+ '/api/health',
19
+ '/auth/oauth/callback',
20
+ '/logout',
21
+ '/api/files/orgs/abc/logo.webp'
22
+ ]) {
23
+ expect(classifier.isPublicRoute(path)).toBe(true);
24
+ }
25
+ });
26
+ it('denies by default — unknown routes are gated', () => {
27
+ for (const path of ['/library', '/api/projects', '/billing', '/whatever']) {
28
+ expect(classifier.isPublicRoute(path)).toBe(false);
29
+ }
30
+ });
31
+ it('exact-match pages do not admit siblings via prefix', () => {
32
+ expect(classifier.isPublicRoute('/login-other')).toBe(false);
33
+ expect(classifier.isPublicRoute('/api/healthz')).toBe(false);
34
+ });
35
+ it('self-gating prefix is not a loose match', () => {
36
+ expect(classifier.isSelfGatingApiRoute('/api/files/x')).toBe(true);
37
+ expect(classifier.isSelfGatingApiRoute('/api/filesX/y')).toBe(false);
38
+ expect(classifier.isSelfGatingApiRoute('/api/projects')).toBe(false);
39
+ });
40
+ it('recognizes static assets by prefix and exact path', () => {
41
+ expect(classifier.isStaticAsset('/_app/immutable/x.js')).toBe(true);
42
+ expect(classifier.isStaticAsset('/favicon/16.png')).toBe(true);
43
+ expect(classifier.isStaticAsset('/robots.txt')).toBe(true);
44
+ expect(classifier.isStaticAsset('/favicon-other.png')).toBe(false);
45
+ expect(classifier.isStaticAsset('/login')).toBe(false);
46
+ });
47
+ it('every allowlist is optional — an empty config gates everything', () => {
48
+ const closed = createRouteClassifier({});
49
+ expect(closed.isPublicRoute('/')).toBe(false);
50
+ expect(closed.isStaticAsset('/robots.txt')).toBe(false);
51
+ expect(closed.isSelfGatingApiRoute('/api/files/x')).toBe(false);
52
+ });
53
+ });
54
+ //# sourceMappingURL=route-classifier.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-classifier.test.js","sourceRoot":"","sources":["../../../src/http/__tests__/route-classifier.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=safe-redirect.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-redirect.test.d.ts","sourceRoot":"","sources":["../../../src/http/__tests__/safe-redirect.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { safeRedirectTarget } from '../redirect.js';
3
+ /**
4
+ * `safeRedirectTarget` is the post-login redirect validator. The point of
5
+ * the function is to reject *exactly* the values that would otherwise
6
+ * smuggle an external host into a `redirect(303, …)` call:
7
+ * protocol-relative URLs (`//evil.com`) and back-slash variants some
8
+ * browsers normalize as a path separator. Each row below is one such case.
9
+ */
10
+ describe('safeRedirectTarget', () => {
11
+ it.each([
12
+ ['/foo', '/foo'],
13
+ ['/foo/bar', '/foo/bar'],
14
+ ['/foo?x=1', '/foo?x=1'],
15
+ ['/foo#frag', '/foo#frag'],
16
+ ['/a', '/a'] // shortest valid path: `/` + non-`/` char
17
+ ])('accepts same-origin path %j → %j', (input, expected) => {
18
+ expect(safeRedirectTarget(input, '/fallback')).toBe(expected);
19
+ });
20
+ it.each([
21
+ ['//evil.com', 'protocol-relative URL'],
22
+ ['//evil.com/path', 'protocol-relative URL with path'],
23
+ ['/\\evil.com', 'back-slash bypass'],
24
+ ['https://evil.com', 'absolute URL'],
25
+ ['http://evil.com', 'absolute URL'],
26
+ ['javascript:alert(1)', 'javascript: scheme'],
27
+ ['', 'empty string'],
28
+ ['/', 'lone slash (length<2)'],
29
+ ['evil.com', 'no leading slash'],
30
+ ['./relative', 'relative path']
31
+ ])('rejects %j (%s)', (input) => {
32
+ expect(safeRedirectTarget(input, '/fallback')).toBe('/fallback');
33
+ });
34
+ it.each([null, undefined])('rejects %j', (input) => {
35
+ expect(safeRedirectTarget(input, '/fallback')).toBe('/fallback');
36
+ });
37
+ it('passes through arbitrary non-string types as fallback', () => {
38
+ // safeRedirectTarget's signature is `string | null | undefined`, but
39
+ // some call sites get `unknown` from FormData — make sure we don't
40
+ // throw on a stray non-string.
41
+ expect(safeRedirectTarget(42, '/fallback')).toBe('/fallback');
42
+ });
43
+ });
44
+ //# sourceMappingURL=safe-redirect.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-redirect.test.js","sourceRoot":"","sources":["../../../src/http/__tests__/safe-redirect.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;;;;GAMG;AACH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,IAAI,CAAC;QACP,CAAC,MAAM,EAAE,MAAM,CAAC;QAChB,CAAC,UAAU,EAAE,UAAU,CAAC;QACxB,CAAC,UAAU,EAAE,UAAU,CAAC;QACxB,CAAC,WAAW,EAAE,WAAW,CAAC;QAC1B,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,0CAA0C;KACvD,CAAC,CAAC,kCAAkC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC1D,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACP,CAAC,YAAY,EAAE,uBAAuB,CAAC;QACvC,CAAC,iBAAiB,EAAE,iCAAiC,CAAC;QACtD,CAAC,aAAa,EAAE,mBAAmB,CAAC;QACpC,CAAC,kBAAkB,EAAE,cAAc,CAAC;QACpC,CAAC,iBAAiB,EAAE,cAAc,CAAC;QACnC,CAAC,qBAAqB,EAAE,oBAAoB,CAAC;QAC7C,CAAC,EAAE,EAAE,cAAc,CAAC;QACpB,CAAC,GAAG,EAAE,uBAAuB,CAAC;QAC9B,CAAC,UAAU,EAAE,kBAAkB,CAAC;QAChC,CAAC,YAAY,EAAE,eAAe,CAAC;KAC/B,CAAC,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,EAAE;QAC/B,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE;QAClD,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAChE,qEAAqE;QACrE,mEAAmE;QACnE,+BAA+B;QAC/B,MAAM,CAAC,kBAAkB,CAAC,EAAuB,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=security-headers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.test.d.ts","sourceRoot":"","sources":["../../../src/http/__tests__/security-headers.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { applySecurityHeaders } from '../security-headers.js';
3
+ describe('applySecurityHeaders', () => {
4
+ it('sets the baseline hardening headers and returns the same response', () => {
5
+ const response = new Response('ok');
6
+ const out = applySecurityHeaders(response, { hsts: false });
7
+ expect(out).toBe(response);
8
+ expect(out.headers.get('X-Content-Type-Options')).toBe('nosniff');
9
+ expect(out.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
10
+ expect(out.headers.get('Permissions-Policy')).toContain('camera=()');
11
+ });
12
+ it('sets HSTS only when asked (production)', () => {
13
+ const dev = applySecurityHeaders(new Response('ok'), { hsts: false });
14
+ expect(dev.headers.get('Strict-Transport-Security')).toBeNull();
15
+ const prod = applySecurityHeaders(new Response('ok'), { hsts: true });
16
+ expect(prod.headers.get('Strict-Transport-Security')).toBe('max-age=31536000; includeSubDomains');
17
+ });
18
+ it('never sets CSP or frame headers (iframe embedding is a product requirement)', () => {
19
+ const out = applySecurityHeaders(new Response('ok'), { hsts: true });
20
+ expect(out.headers.get('Content-Security-Policy')).toBeNull();
21
+ expect(out.headers.get('X-Frame-Options')).toBeNull();
22
+ });
23
+ });
24
+ //# sourceMappingURL=security-headers.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.test.js","sourceRoot":"","sources":["../../../src/http/__tests__/security-headers.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC5E,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,oBAAoB,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACnF,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QACjD,MAAM,GAAG,GAAG,oBAAoB,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAEhE,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CACzD,qCAAqC,CACrC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACtF,MAAM,GAAG,GAAG,oBAAoB,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvD,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ /** Minimal read-side view of a request's headers — `Headers` satisfies it. */
2
+ export interface HeadersLike {
3
+ get(name: string): string | null;
4
+ }
5
+ /**
6
+ * True when the request's declared `Content-Length` exceeds `maxBytes`.
7
+ * Check BEFORE the body is read so a malicious client can't burn memory by
8
+ * sending a huge payload to a small-body endpoint; the route maps a `true`
9
+ * result to its transport's 413.
10
+ *
11
+ * Background: a global body-size limit (e.g. adapter-node's `BODY_SIZE_LIMIT`)
12
+ * has to be set high enough for the largest legitimate upload, which means
13
+ * smaller JSON endpoints inherit that ceiling by default. This check is the
14
+ * per-route lower bound.
15
+ *
16
+ * Caveat: requests without `Content-Length` (chunked transfer encoding) bypass
17
+ * this check. Most browsers and HTTP clients send Content-Length on POST/PUT.
18
+ * The global limit is the backstop for those.
19
+ */
20
+ export declare function declaredBodySizeExceeds(headers: HeadersLike, maxBytes: number): boolean;
21
+ //# sourceMappingURL=body-size.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-size.d.ts","sourceRoot":"","sources":["../../src/http/body-size.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGvF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * True when the request's declared `Content-Length` exceeds `maxBytes`.
3
+ * Check BEFORE the body is read so a malicious client can't burn memory by
4
+ * sending a huge payload to a small-body endpoint; the route maps a `true`
5
+ * result to its transport's 413.
6
+ *
7
+ * Background: a global body-size limit (e.g. adapter-node's `BODY_SIZE_LIMIT`)
8
+ * has to be set high enough for the largest legitimate upload, which means
9
+ * smaller JSON endpoints inherit that ceiling by default. This check is the
10
+ * per-route lower bound.
11
+ *
12
+ * Caveat: requests without `Content-Length` (chunked transfer encoding) bypass
13
+ * this check. Most browsers and HTTP clients send Content-Length on POST/PUT.
14
+ * The global limit is the backstop for those.
15
+ */
16
+ export function declaredBodySizeExceeds(headers, maxBytes) {
17
+ const declared = Number(headers.get('content-length'));
18
+ return Number.isFinite(declared) && declared > maxBytes;
19
+ }
20
+ //# sourceMappingURL=body-size.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-size.js","sourceRoot":"","sources":["../../src/http/body-size.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAoB,EAAE,QAAgB;IAC7E,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,QAAQ,CAAC;AACzD,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { safeRedirectTarget } from './redirect.js';
2
+ export { declaredBodySizeExceeds, type HeadersLike } from './body-size.js';
3
+ export { applySecurityHeaders, type SecurityHeaderOptions } from './security-headers.js';
4
+ export { createRouteClassifier, type RouteClassifier, type RouteClassifierConfig } from './route-classifier.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,6 @@
1
+ // HTTP hardening helpers — transport-agnostic request/response guards.
2
+ export { safeRedirectTarget } from './redirect.js';
3
+ export { declaredBodySizeExceeds } from './body-size.js';
4
+ export { applySecurityHeaders } from './security-headers.js';
5
+ export { createRouteClassifier } from './route-classifier.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Validate a user-supplied post-login redirect target. Accepts only same-origin
3
+ * relative paths starting with `/` followed by a non-`/` character, so
4
+ * `//evil.com/path` (protocol-relative URL — browser treats as cross-origin)
5
+ * and `/\evil.com` (back-slash variants some browsers normalize) are rejected.
6
+ *
7
+ * Always returns a safe path: the validated target on success, the fallback
8
+ * otherwise. Routes call this with `redirectTo` from form data or query string.
9
+ */
10
+ export declare function safeRedirectTarget(raw: string | null | undefined, fallback: string): string;
11
+ //# sourceMappingURL=redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/http/redirect.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM3F"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Validate a user-supplied post-login redirect target. Accepts only same-origin
3
+ * relative paths starting with `/` followed by a non-`/` character, so
4
+ * `//evil.com/path` (protocol-relative URL — browser treats as cross-origin)
5
+ * and `/\evil.com` (back-slash variants some browsers normalize) are rejected.
6
+ *
7
+ * Always returns a safe path: the validated target on success, the fallback
8
+ * otherwise. Routes call this with `redirectTo` from form data or query string.
9
+ */
10
+ export function safeRedirectTarget(raw, fallback) {
11
+ if (typeof raw !== 'string' || raw.length < 2)
12
+ return fallback;
13
+ if (raw[0] !== '/')
14
+ return fallback;
15
+ // Reject protocol-relative (`//host`) and back-slash bypass (`/\host`).
16
+ if (raw[1] === '/' || raw[1] === '\\')
17
+ return fallback;
18
+ return raw;
19
+ }
20
+ //# sourceMappingURL=redirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/http/redirect.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAA8B,EAAE,QAAgB;IAClF,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC/D,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG;QAAE,OAAO,QAAQ,CAAC;IACpC,wEAAwE;IACxE,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvD,OAAO,GAAG,CAAC;AACZ,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Deny-by-default route classification. Every request is gated unless its
3
+ * path matches one of the explicit allowlists supplied here — so adding a new
4
+ * top-level route is safe: forgetting to update the lists makes it gated (a
5
+ * loud 401/redirect), not silently public.
6
+ *
7
+ * The *pattern* is reusable; the *values* are app policy — a consuming app
8
+ * supplies its own route sets. All matching is plain exact/prefix string
9
+ * comparison: cheap enough for the hooks hot path, and no regex to get wrong.
10
+ */
11
+ export interface RouteClassifierConfig {
12
+ /** Exact-match public pages — no session needed (e.g. `/`, `/login`). */
13
+ publicPages?: Iterable<string>;
14
+ /**
15
+ * Public path prefixes (e.g. `/auth/` for OAuth start + callback — the
16
+ * user has no session yet by definition).
17
+ */
18
+ publicPrefixes?: readonly string[];
19
+ /**
20
+ * Exact-match API endpoints that must answer without a session (e.g. a
21
+ * load-balancer health probe). Anything added here must be safe to expose
22
+ * to anonymous callers.
23
+ */
24
+ publicApis?: Iterable<string>;
25
+ /**
26
+ * A single prefix whose routes apply their own per-request authorization
27
+ * (e.g. a blob proxy classifying each asset path). The hook must NOT deny
28
+ * these up front; they classify as public (best-effort session attach) and
29
+ * the route makes the real decision. Keep this to ONE prefix — every other
30
+ * API route stays deny-by-default.
31
+ */
32
+ selfGatingPrefix?: string;
33
+ /** Static-asset path prefixes the adapter serves directly (e.g. `/_app/`). */
34
+ staticPrefixes?: readonly string[];
35
+ /** Exact static-asset paths (e.g. `/favicon.svg`, `/robots.txt`). */
36
+ staticPaths?: Iterable<string>;
37
+ }
38
+ export interface RouteClassifier {
39
+ /** Static assets bypass auth gating; callers may add cache headers. */
40
+ isStaticAsset(pathname: string): boolean;
41
+ /** True for paths under `selfGatingPrefix` — the route gates itself. */
42
+ isSelfGatingApiRoute(pathname: string): boolean;
43
+ /** True when the path needs no session: pages, APIs, prefixes, self-gating. */
44
+ isPublicRoute(pathname: string): boolean;
45
+ }
46
+ export declare function createRouteClassifier(config: RouteClassifierConfig): RouteClassifier;
47
+ //# sourceMappingURL=route-classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-classifier.d.ts","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,qBAAqB;IACrC,yEAAyE;IACzE,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qEAAqE;IACrE,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC/B,uEAAuE;IACvE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACzC,wEAAwE;IACxE,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,+EAA+E;IAC/E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACzC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAyBpF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Deny-by-default route classification. Every request is gated unless its
3
+ * path matches one of the explicit allowlists supplied here — so adding a new
4
+ * top-level route is safe: forgetting to update the lists makes it gated (a
5
+ * loud 401/redirect), not silently public.
6
+ *
7
+ * The *pattern* is reusable; the *values* are app policy — a consuming app
8
+ * supplies its own route sets. All matching is plain exact/prefix string
9
+ * comparison: cheap enough for the hooks hot path, and no regex to get wrong.
10
+ */
11
+ export function createRouteClassifier(config) {
12
+ const publicPages = new Set(config.publicPages ?? []);
13
+ const publicPrefixes = config.publicPrefixes ?? [];
14
+ const publicApis = new Set(config.publicApis ?? []);
15
+ const selfGatingPrefix = config.selfGatingPrefix;
16
+ const staticPrefixes = config.staticPrefixes ?? [];
17
+ const staticPaths = new Set(config.staticPaths ?? []);
18
+ function isStaticAsset(pathname) {
19
+ if (staticPaths.has(pathname))
20
+ return true;
21
+ return staticPrefixes.some((p) => pathname.startsWith(p));
22
+ }
23
+ function isSelfGatingApiRoute(pathname) {
24
+ return selfGatingPrefix !== undefined && pathname.startsWith(selfGatingPrefix);
25
+ }
26
+ function isPublicRoute(pathname) {
27
+ if (publicPages.has(pathname))
28
+ return true;
29
+ if (publicApis.has(pathname))
30
+ return true;
31
+ if (isSelfGatingApiRoute(pathname))
32
+ return true;
33
+ return publicPrefixes.some((p) => pathname.startsWith(p));
34
+ }
35
+ return { isStaticAsset, isSelfGatingApiRoute, isPublicRoute };
36
+ }
37
+ //# sourceMappingURL=route-classifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-classifier.js","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAuCH,MAAM,UAAU,qBAAqB,CAAC,MAA6B;IAClE,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACnD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACjD,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACnD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAEtD,SAAS,aAAa,CAAC,QAAgB;QACtC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,SAAS,oBAAoB,CAAC,QAAgB;QAC7C,OAAO,gBAAgB,KAAK,SAAS,IAAI,QAAQ,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAChF,CAAC;IAED,SAAS,aAAa,CAAC,QAAgB;QACtC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,oBAAoB,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAChD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,aAAa,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,31 @@
1
+ export interface SecurityHeaderOptions {
2
+ /**
3
+ * Set `Strict-Transport-Security` — production only. Locking dev
4
+ * (http://localhost) into HTTPS would brick local development, so the
5
+ * caller decides what "production" means (e.g. `NODE_ENV`).
6
+ */
7
+ hsts: boolean;
8
+ }
9
+ /**
10
+ * Apply baseline browser-hardening headers to a response. Cheap wins that
11
+ * don't require UI verification:
12
+ *
13
+ * - X-Content-Type-Options: nosniff — disables MIME sniffing.
14
+ * - Referrer-Policy: strict-origin-when-cross-origin — strips the path
15
+ * and query from cross-origin Referer headers; matches modern browser
16
+ * defaults but pins the behavior in case the default ever drifts.
17
+ * - Permissions-Policy: opt out of browser APIs Selva-engine apps don't
18
+ * use, so XSS can't enable them.
19
+ * - HSTS when `opts.hsts` (production).
20
+ *
21
+ * **Intentionally NOT set here:**
22
+ * - Content-Security-Policy + frame-ancestors. Selva-engine apps are built
23
+ * for iframe embedding, so a strict CSP needs UI-phase validation against
24
+ * real consumer sites before it can ship.
25
+ * - X-Frame-Options. Same iframe-embedding constraint.
26
+ * - Cache-Control. Asset-path layout is app policy; set it in the caller.
27
+ *
28
+ * Returns the same response for chaining.
29
+ */
30
+ export declare function applySecurityHeaders(response: Response, opts: SecurityHeaderOptions): Response;
31
+ //# sourceMappingURL=security-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,IAAI,EAAE,OAAO,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,qBAAqB,GAAG,QAAQ,CAW9F"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Apply baseline browser-hardening headers to a response. Cheap wins that
3
+ * don't require UI verification:
4
+ *
5
+ * - X-Content-Type-Options: nosniff — disables MIME sniffing.
6
+ * - Referrer-Policy: strict-origin-when-cross-origin — strips the path
7
+ * and query from cross-origin Referer headers; matches modern browser
8
+ * defaults but pins the behavior in case the default ever drifts.
9
+ * - Permissions-Policy: opt out of browser APIs Selva-engine apps don't
10
+ * use, so XSS can't enable them.
11
+ * - HSTS when `opts.hsts` (production).
12
+ *
13
+ * **Intentionally NOT set here:**
14
+ * - Content-Security-Policy + frame-ancestors. Selva-engine apps are built
15
+ * for iframe embedding, so a strict CSP needs UI-phase validation against
16
+ * real consumer sites before it can ship.
17
+ * - X-Frame-Options. Same iframe-embedding constraint.
18
+ * - Cache-Control. Asset-path layout is app policy; set it in the caller.
19
+ *
20
+ * Returns the same response for chaining.
21
+ */
22
+ export function applySecurityHeaders(response, opts) {
23
+ response.headers.set('X-Content-Type-Options', 'nosniff');
24
+ response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
25
+ response.headers.set('Permissions-Policy', 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()');
26
+ if (opts.hsts) {
27
+ response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
28
+ }
29
+ return response;
30
+ }
31
+ //# sourceMappingURL=security-headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAkB,EAAE,IAA2B;IACnF,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,iCAAiC,CAAC,CAAC;IAC3E,QAAQ,CAAC,OAAO,CAAC,GAAG,CACnB,oBAAoB,EACpB,+GAA+G,CAC/G,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,qCAAqC,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -9,10 +9,19 @@
9
9
  *
10
10
  * The full compute subsurface is also available at `@selvajs/server/compute`,
11
11
  * the definitions slice (DefinitionService, schema extraction/validation,
12
- * render loader) at `@selvajs/server/definitions`, and the provider wiring
13
- * (registry-driven `createSelvaProviders`) at `@selvajs/server/providers`.
12
+ * render loader) at `@selvajs/server/definitions`, the provider wiring
13
+ * (registry-driven `createSelvaProviders`) at `@selvajs/server/providers`,
14
+ * the HMAC token codec at `@selvajs/server/tokens`, error reporting at
15
+ * `@selvajs/server/errors`, HTTP hardening helpers at `@selvajs/server/http`,
16
+ * access-rule input assembly at `@selvajs/server/access`, and deployment/ops
17
+ * helpers at `@selvajs/server/ops`.
14
18
  */
15
19
  export * from './compute/index.js';
16
20
  export * from './definitions/index.js';
17
21
  export * from './providers/index.js';
22
+ export * from './tokens/index.js';
23
+ export * from './errors/index.js';
24
+ export * from './http/index.js';
25
+ export * from './access/index.js';
26
+ export * from './ops/index.js';
18
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC"}