@actuate-media/cms-core 0.10.4 → 0.11.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 (125) hide show
  1. package/dist/__tests__/api/admin-contracts.test.js +1 -0
  2. package/dist/__tests__/api/admin-contracts.test.js.map +1 -1
  3. package/dist/__tests__/api/public-globals.test.js +8 -4
  4. package/dist/__tests__/api/public-globals.test.js.map +1 -1
  5. package/dist/__tests__/security/audit.test.d.ts +2 -0
  6. package/dist/__tests__/security/audit.test.d.ts.map +1 -0
  7. package/dist/__tests__/security/audit.test.js +50 -0
  8. package/dist/__tests__/security/audit.test.js.map +1 -0
  9. package/dist/__tests__/security/client-ip.test.d.ts +2 -0
  10. package/dist/__tests__/security/client-ip.test.d.ts.map +1 -0
  11. package/dist/__tests__/security/client-ip.test.js +37 -0
  12. package/dist/__tests__/security/client-ip.test.js.map +1 -0
  13. package/dist/__tests__/security/ip-allowlist.test.d.ts +2 -0
  14. package/dist/__tests__/security/ip-allowlist.test.d.ts.map +1 -0
  15. package/dist/__tests__/security/ip-allowlist.test.js +40 -0
  16. package/dist/__tests__/security/ip-allowlist.test.js.map +1 -0
  17. package/dist/__tests__/security/redact.test.d.ts +2 -0
  18. package/dist/__tests__/security/redact.test.d.ts.map +1 -0
  19. package/dist/__tests__/security/redact.test.js +31 -0
  20. package/dist/__tests__/security/redact.test.js.map +1 -0
  21. package/dist/__tests__/security/secret-storage.test.d.ts +2 -0
  22. package/dist/__tests__/security/secret-storage.test.d.ts.map +1 -0
  23. package/dist/__tests__/security/secret-storage.test.js +42 -0
  24. package/dist/__tests__/security/secret-storage.test.js.map +1 -0
  25. package/dist/__tests__/security/upload-magic.test.d.ts +2 -0
  26. package/dist/__tests__/security/upload-magic.test.d.ts.map +1 -0
  27. package/dist/__tests__/security/upload-magic.test.js +55 -0
  28. package/dist/__tests__/security/upload-magic.test.js.map +1 -0
  29. package/dist/__tests__/server-site.test.d.ts +2 -0
  30. package/dist/__tests__/server-site.test.d.ts.map +1 -0
  31. package/dist/__tests__/server-site.test.js +123 -0
  32. package/dist/__tests__/server-site.test.js.map +1 -0
  33. package/dist/actions.d.ts.map +1 -1
  34. package/dist/actions.js +170 -34
  35. package/dist/actions.js.map +1 -1
  36. package/dist/api/handler-factory.d.ts.map +1 -1
  37. package/dist/api/handler-factory.js +64 -9
  38. package/dist/api/handler-factory.js.map +1 -1
  39. package/dist/api/handlers.d.ts.map +1 -1
  40. package/dist/api/handlers.js +673 -116
  41. package/dist/api/handlers.js.map +1 -1
  42. package/dist/api/openapi.d.ts.map +1 -1
  43. package/dist/api/openapi.js +38 -0
  44. package/dist/api/openapi.js.map +1 -1
  45. package/dist/auth/mfa-pending.d.ts +24 -0
  46. package/dist/auth/mfa-pending.d.ts.map +1 -0
  47. package/dist/auth/mfa-pending.js +38 -0
  48. package/dist/auth/mfa-pending.js.map +1 -0
  49. package/dist/auth/oauth.d.ts +25 -3
  50. package/dist/auth/oauth.d.ts.map +1 -1
  51. package/dist/auth/oauth.js +109 -20
  52. package/dist/auth/oauth.js.map +1 -1
  53. package/dist/auth/reset.d.ts.map +1 -1
  54. package/dist/auth/reset.js +26 -2
  55. package/dist/auth/reset.js.map +1 -1
  56. package/dist/auth/session.d.ts +9 -2
  57. package/dist/auth/session.d.ts.map +1 -1
  58. package/dist/auth/session.js +20 -2
  59. package/dist/auth/session.js.map +1 -1
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/middleware.d.ts.map +1 -1
  65. package/dist/middleware.js +21 -34
  66. package/dist/middleware.js.map +1 -1
  67. package/dist/page-builder/__tests__/blocks.test.js +104 -1
  68. package/dist/page-builder/__tests__/blocks.test.js.map +1 -1
  69. package/dist/page-builder/blocks.d.ts +18 -1
  70. package/dist/page-builder/blocks.d.ts.map +1 -1
  71. package/dist/page-builder/blocks.js +22 -2
  72. package/dist/page-builder/blocks.js.map +1 -1
  73. package/dist/security/audit.d.ts.map +1 -1
  74. package/dist/security/audit.js +8 -4
  75. package/dist/security/audit.js.map +1 -1
  76. package/dist/security/client-ip.d.ts +33 -0
  77. package/dist/security/client-ip.d.ts.map +1 -0
  78. package/dist/security/client-ip.js +39 -0
  79. package/dist/security/client-ip.js.map +1 -0
  80. package/dist/security/index.d.ts +7 -0
  81. package/dist/security/index.d.ts.map +1 -1
  82. package/dist/security/index.js +5 -0
  83. package/dist/security/index.js.map +1 -1
  84. package/dist/security/internal-keys.d.ts +15 -0
  85. package/dist/security/internal-keys.d.ts.map +1 -0
  86. package/dist/security/internal-keys.js +33 -0
  87. package/dist/security/internal-keys.js.map +1 -0
  88. package/dist/security/ip-allowlist.d.ts +13 -1
  89. package/dist/security/ip-allowlist.d.ts.map +1 -1
  90. package/dist/security/ip-allowlist.js +120 -12
  91. package/dist/security/ip-allowlist.js.map +1 -1
  92. package/dist/security/rate-limit.d.ts.map +1 -1
  93. package/dist/security/rate-limit.js +49 -17
  94. package/dist/security/rate-limit.js.map +1 -1
  95. package/dist/security/redact.d.ts +12 -0
  96. package/dist/security/redact.d.ts.map +1 -0
  97. package/dist/security/redact.js +41 -0
  98. package/dist/security/redact.js.map +1 -0
  99. package/dist/security/safe-fetch.d.ts +35 -0
  100. package/dist/security/safe-fetch.d.ts.map +1 -0
  101. package/dist/security/safe-fetch.js +45 -0
  102. package/dist/security/safe-fetch.js.map +1 -0
  103. package/dist/security/secret-storage.d.ts +22 -0
  104. package/dist/security/secret-storage.d.ts.map +1 -0
  105. package/dist/security/secret-storage.js +75 -0
  106. package/dist/security/secret-storage.js.map +1 -0
  107. package/dist/security/upload.d.ts +23 -4
  108. package/dist/security/upload.d.ts.map +1 -1
  109. package/dist/security/upload.js +110 -21
  110. package/dist/security/upload.js.map +1 -1
  111. package/dist/server-site.d.ts +54 -0
  112. package/dist/server-site.d.ts.map +1 -0
  113. package/dist/server-site.js +149 -0
  114. package/dist/server-site.js.map +1 -0
  115. package/dist/site.d.ts.map +1 -1
  116. package/dist/site.js +19 -1
  117. package/dist/site.js.map +1 -1
  118. package/dist/storage/index.d.ts +20 -10
  119. package/dist/storage/index.d.ts.map +1 -1
  120. package/dist/storage/index.js +6 -3
  121. package/dist/storage/index.js.map +1 -1
  122. package/dist/webhooks/index.d.ts.map +1 -1
  123. package/dist/webhooks/index.js +20 -9
  124. package/dist/webhooks/index.js.map +1 -1
  125. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG7H,OAAO,EAAE,aAAa,IAAI,iBAAiB,EAAE,aAAa,IAAI,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG7E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG7H,OAAO,EAAE,aAAa,IAAI,iBAAiB,EAAE,aAAa,IAAI,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEnE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG7E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Centralised list of "system" keys that may appear inside `Document.data`
3
+ * but are managed by the CMS rather than the user. Anything here is:
4
+ *
5
+ * - stripped from API responses (so internal scoring data isn't leaked)
6
+ * - allowed through field-level write access checks (so plugins can
7
+ * set them even when the field isn't declared in the collection)
8
+ *
9
+ * Keep this in lockstep with `actions.ts`/`access.ts` to avoid drift.
10
+ */
11
+ export declare const INTERNAL_DATA_KEYS: Set<string>;
12
+ export declare function isInternalDataKey(key: string): boolean;
13
+ /** Return a shallow copy of `data` with all internal/system keys removed. */
14
+ export declare function stripInternalDataKeys<T extends Record<string, unknown>>(data: T): T;
15
+ //# sourceMappingURL=internal-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-keys.d.ts","sourceRoot":"","sources":["../../src/security/internal-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,aAQ7B,CAAC;AAEH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAOnF"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Centralised list of "system" keys that may appear inside `Document.data`
3
+ * but are managed by the CMS rather than the user. Anything here is:
4
+ *
5
+ * - stripped from API responses (so internal scoring data isn't leaked)
6
+ * - allowed through field-level write access checks (so plugins can
7
+ * set them even when the field isn't declared in the collection)
8
+ *
9
+ * Keep this in lockstep with `actions.ts`/`access.ts` to avoid drift.
10
+ */
11
+ export const INTERNAL_DATA_KEYS = new Set([
12
+ '_layout',
13
+ '_pageSettings',
14
+ '_aiScore',
15
+ '_embedding',
16
+ '_seoScore',
17
+ '_brandScore',
18
+ '_readabilityScore',
19
+ ]);
20
+ export function isInternalDataKey(key) {
21
+ return key.startsWith('_') || INTERNAL_DATA_KEYS.has(key);
22
+ }
23
+ /** Return a shallow copy of `data` with all internal/system keys removed. */
24
+ export function stripInternalDataKeys(data) {
25
+ const out = {};
26
+ for (const [key, value] of Object.entries(data)) {
27
+ if (isInternalDataKey(key))
28
+ continue;
29
+ out[key] = value;
30
+ }
31
+ return out;
32
+ }
33
+ //# sourceMappingURL=internal-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-keys.js","sourceRoot":"","sources":["../../src/security/internal-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAS;IAChD,SAAS;IACT,eAAe;IACf,UAAU;IACV,YAAY;IACZ,WAAW;IACX,aAAa;IACb,mBAAmB;CACpB,CAAC,CAAC;AAEH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAoC,IAAO;IAC9E,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,iBAAiB,CAAC,GAAG,CAAC;YAAE,SAAS;QACrC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC;IACD,OAAO,GAAQ,CAAC;AAClB,CAAC"}
@@ -1,3 +1,15 @@
1
- /** Check whether an IP address is within a list of allowed IPs or CIDR ranges. */
1
+ /**
2
+ * IP allowlist matcher supporting both IPv4 and IPv6, including CIDR ranges.
3
+ *
4
+ * - Empty allowlist allows everything (the integrator opted out).
5
+ * - When the allowlist is non-empty, the literal `'unknown'` (the sentinel
6
+ * returned by the trusted-IP resolver when no proxy header was present)
7
+ * never matches.
8
+ * - IPv4 entries match IPv4 addresses (with optional `/n` CIDR mask, n=0..32).
9
+ * - IPv6 entries match IPv6 addresses (with optional `/n` CIDR mask, n=0..128).
10
+ * Two IPv6 strings are equal when their canonical 16-byte form matches —
11
+ * so `2001:db8::1` matches `2001:db8:0:0:0:0:0:1`.
12
+ * - Mapped IPv4-in-IPv6 addresses (`::ffff:1.2.3.4`) are normalised to IPv4.
13
+ */
2
14
  export declare function isIpAllowed(ip: string, allowlist: string[]): boolean;
3
15
  //# sourceMappingURL=ip-allowlist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ip-allowlist.d.ts","sourceRoot":"","sources":["../../src/security/ip-allowlist.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,WAAW,CACzB,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAWT"}
1
+ {"version":3,"file":"ip-allowlist.d.ts","sourceRoot":"","sources":["../../src/security/ip-allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAcpE"}
@@ -1,35 +1,143 @@
1
- /** Check whether an IP address is within a list of allowed IPs or CIDR ranges. */
1
+ /**
2
+ * IP allowlist matcher supporting both IPv4 and IPv6, including CIDR ranges.
3
+ *
4
+ * - Empty allowlist allows everything (the integrator opted out).
5
+ * - When the allowlist is non-empty, the literal `'unknown'` (the sentinel
6
+ * returned by the trusted-IP resolver when no proxy header was present)
7
+ * never matches.
8
+ * - IPv4 entries match IPv4 addresses (with optional `/n` CIDR mask, n=0..32).
9
+ * - IPv6 entries match IPv6 addresses (with optional `/n` CIDR mask, n=0..128).
10
+ * Two IPv6 strings are equal when their canonical 16-byte form matches —
11
+ * so `2001:db8::1` matches `2001:db8:0:0:0:0:0:1`.
12
+ * - Mapped IPv4-in-IPv6 addresses (`::ffff:1.2.3.4`) are normalised to IPv4.
13
+ */
2
14
  export function isIpAllowed(ip, allowlist) {
3
15
  if (allowlist.length === 0)
4
16
  return true;
17
+ if (!ip || ip === 'unknown')
18
+ return false;
19
+ const normalised = normaliseIp(ip);
5
20
  for (const entry of allowlist) {
6
- if (entry.includes("/")) {
7
- if (isInCidr(ip, entry))
21
+ if (entry.includes('/')) {
22
+ if (isInCidr(normalised, entry))
8
23
  return true;
9
24
  }
10
- else if (ip === entry) {
25
+ else if (ipsEqual(normalised, normaliseIp(entry))) {
11
26
  return true;
12
27
  }
13
28
  }
14
29
  return false;
15
30
  }
31
+ function ipsEqual(a, b) {
32
+ if (a === b)
33
+ return true;
34
+ // Compare IPv6 by canonical bytes so `2001:db8::1` === `2001:db8:0:0:0:0:0:1`.
35
+ if (a.includes(':') && b.includes(':')) {
36
+ const ca = canonicalIpv6(a);
37
+ const cb = canonicalIpv6(b);
38
+ return ca !== null && cb !== null && ca === cb;
39
+ }
40
+ return false;
41
+ }
42
+ function canonicalIpv6(ip) {
43
+ const bytes = ipv6ToBytes(ip);
44
+ if (!bytes)
45
+ return null;
46
+ let out = '';
47
+ for (let i = 0; i < 16; i++) {
48
+ out += (bytes[i] ?? 0).toString(16).padStart(2, '0');
49
+ }
50
+ return out;
51
+ }
52
+ function normaliseIp(ip) {
53
+ const trimmed = ip.trim();
54
+ // ::ffff:1.2.3.4 → 1.2.3.4
55
+ if (trimmed.toLowerCase().startsWith('::ffff:')) {
56
+ const v4 = trimmed.slice(7);
57
+ if (isValidIPv4(v4))
58
+ return v4;
59
+ }
60
+ return trimmed;
61
+ }
62
+ function isValidIPv4(s) {
63
+ const parts = s.split('.').map(Number);
64
+ return parts.length === 4 && parts.every((p) => Number.isInteger(p) && p >= 0 && p <= 255);
65
+ }
16
66
  function isInCidr(ip, cidr) {
17
- const [range, bitsStr] = cidr.split("/");
67
+ const [range, bitsStr] = cidr.split('/');
18
68
  if (!range || !bitsStr)
19
69
  return false;
20
70
  const bits = parseInt(bitsStr, 10);
21
- const ipNum = ipToNumber(ip);
22
- const rangeNum = ipToNumber(range);
71
+ if (Number.isNaN(bits))
72
+ return false;
73
+ if (ip.includes(':') || range.includes(':')) {
74
+ return matchIpv6Cidr(ip, range, bits);
75
+ }
76
+ return matchIpv4Cidr(ip, range, bits);
77
+ }
78
+ function matchIpv4Cidr(ip, range, bits) {
79
+ if (bits < 0 || bits > 32)
80
+ return false;
81
+ const ipNum = ipv4ToNumber(ip);
82
+ const rangeNum = ipv4ToNumber(range);
23
83
  if (ipNum === null || rangeNum === null)
24
84
  return false;
25
- const mask = ~((1 << (32 - bits)) - 1) >>> 0;
85
+ if (bits === 0)
86
+ return true;
87
+ const mask = (~((1 << (32 - bits)) - 1)) >>> 0;
26
88
  return (ipNum & mask) === (rangeNum & mask);
27
89
  }
28
- function ipToNumber(ip) {
29
- const parts = ip.split(".").map(Number);
30
- if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) {
90
+ function ipv4ToNumber(ip) {
91
+ if (!isValidIPv4(ip))
31
92
  return null;
32
- }
93
+ const parts = ip.split('.').map(Number);
33
94
  return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
34
95
  }
96
+ function matchIpv6Cidr(ip, range, bits) {
97
+ if (bits < 0 || bits > 128)
98
+ return false;
99
+ const ipBytes = ipv6ToBytes(ip);
100
+ const rangeBytes = ipv6ToBytes(range);
101
+ if (!ipBytes || !rangeBytes)
102
+ return false;
103
+ let remaining = bits;
104
+ for (let i = 0; i < 16 && remaining > 0; i++) {
105
+ const take = Math.min(8, remaining);
106
+ const mask = (0xff << (8 - take)) & 0xff;
107
+ if ((ipBytes[i] & mask) !== (rangeBytes[i] & mask))
108
+ return false;
109
+ remaining -= take;
110
+ }
111
+ return true;
112
+ }
113
+ /** Expand the `::` shorthand and return 16 bytes; null when the input isn't a valid IPv6. */
114
+ function ipv6ToBytes(ip) {
115
+ // Reject IPv4-mapped form for the IPv6 path; caller normalises ::ffff:x.x.x.x first.
116
+ if (ip.includes('.'))
117
+ return null;
118
+ const halves = ip.split('::');
119
+ if (halves.length > 2)
120
+ return null;
121
+ const left = halves[0] ? halves[0].split(':') : [];
122
+ const groups = halves.length === 2
123
+ ? fillGroups(left, halves[1] ? halves[1].split(':') : [])
124
+ : left;
125
+ if (groups.length !== 8)
126
+ return null;
127
+ const bytes = new Uint8Array(16);
128
+ for (let i = 0; i < 8; i++) {
129
+ const value = parseInt(groups[i] || '0', 16);
130
+ if (Number.isNaN(value) || value < 0 || value > 0xffff)
131
+ return null;
132
+ bytes[i * 2] = (value >> 8) & 0xff;
133
+ bytes[i * 2 + 1] = value & 0xff;
134
+ }
135
+ return bytes;
136
+ }
137
+ function fillGroups(left, right) {
138
+ const fill = 8 - left.length - right.length;
139
+ if (fill < 0)
140
+ return [];
141
+ return [...left, ...new Array(fill).fill('0'), ...right];
142
+ }
35
143
  //# sourceMappingURL=ip-allowlist.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ip-allowlist.js","sourceRoot":"","sources":["../../src/security/ip-allowlist.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,UAAU,WAAW,CACzB,EAAU,EACV,SAAmB;IAEnB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAExC,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;QACvC,CAAC;aAAM,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,EAAU,EAAE,IAAY;IACxC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAErC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAEtD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,UAAU,CAAC,EAAU;IAC5B,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,IAAI,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,CAAC;AACtF,CAAC"}
1
+ {"version":3,"file":"ip-allowlist.js","sourceRoot":"","sources":["../../src/security/ip-allowlist.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC7C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4FAA4F;AAC5F,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CA4B9E;AAED,iFAAiF;AACjF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAiD7E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAStE"}
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC7C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4FAA4F;AAC5F,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CA4B9E;AAED,iFAAiF;AACjF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAqF7E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAStE"}
@@ -31,38 +31,70 @@ export function createUpstashRateLimiter(config) {
31
31
  if (!url || !token) {
32
32
  throw new Error('UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are required');
33
33
  }
34
- async function redisCommand(command) {
35
- const response = await fetch(`${url}`, {
34
+ async function redisPipeline(commands) {
35
+ // Upstash exposes pipelined commands via the /pipeline endpoint. Sending
36
+ // INCR + EXPIRE in a single round-trip eliminates the race where the
37
+ // process dies after INCR (or where EXPIRE silently fails) and leaves a
38
+ // counter that never resets — a subtle DoS where one unlucky user is
39
+ // permanently locked out.
40
+ const response = await fetch(`${url}/pipeline`, {
36
41
  method: 'POST',
37
42
  headers: {
38
43
  Authorization: `Bearer ${token}`,
39
44
  'Content-Type': 'application/json',
40
45
  },
41
- body: JSON.stringify(command),
46
+ body: JSON.stringify(commands),
42
47
  });
48
+ if (!response.ok) {
49
+ throw new Error(`Upstash pipeline returned ${response.status}`);
50
+ }
43
51
  const data = await response.json();
44
- return data?.result;
52
+ return data.map((entry) => entry?.result);
45
53
  }
46
54
  const windowSec = Math.ceil(config.windowMs / 1000);
47
55
  return {
48
56
  async check(key) {
49
57
  const redisKey = `ratelimit:${key}`;
50
- const count = await redisCommand(['INCR', redisKey]);
51
- if (count === 1) {
52
- await redisCommand(['EXPIRE', redisKey, String(windowSec)]);
58
+ try {
59
+ const [count, _expireResult, ttl] = await redisPipeline([
60
+ ['INCR', redisKey],
61
+ ['EXPIRE', redisKey, String(windowSec), 'NX'], // only set when no TTL exists
62
+ ['TTL', redisKey],
63
+ ]);
64
+ // Belt-and-braces: if Redis reports the key with no TTL, set one. This
65
+ // can only happen if the EXPIRE NX above wasn't supported, in which
66
+ // case we still want to bound the counter's lifetime.
67
+ if (ttl < 0) {
68
+ await redisPipeline([['EXPIRE', redisKey, String(windowSec)]]).catch(() => undefined);
69
+ }
70
+ const effectiveTtl = ttl > 0 ? ttl : windowSec;
71
+ const resetAt = new Date(Date.now() + effectiveTtl * 1000);
72
+ const allowed = count <= config.maxRequests;
73
+ return {
74
+ allowed,
75
+ remaining: Math.max(0, config.maxRequests - count),
76
+ resetAt,
77
+ retryAfter: allowed ? undefined : effectiveTtl,
78
+ };
79
+ }
80
+ catch (err) {
81
+ // Fail open with logging — a Redis outage should NOT take the entire
82
+ // CMS offline. Audit-level logging makes the degradation visible.
83
+ console.error('[actuate][rate-limit] Upstash request failed, allowing through:', err instanceof Error ? err.message : err);
84
+ return {
85
+ allowed: true,
86
+ remaining: Math.max(0, config.maxRequests - 1),
87
+ resetAt: new Date(Date.now() + config.windowMs),
88
+ };
53
89
  }
54
- const ttl = await redisCommand(['TTL', redisKey]);
55
- const resetAt = new Date(Date.now() + (ttl > 0 ? ttl * 1000 : config.windowMs));
56
- const allowed = count <= config.maxRequests;
57
- return {
58
- allowed,
59
- remaining: Math.max(0, config.maxRequests - count),
60
- resetAt,
61
- retryAfter: allowed ? undefined : ttl > 0 ? ttl : Math.ceil(config.windowMs / 1000),
62
- };
63
90
  },
64
91
  async reset(key) {
65
- await redisCommand(['DEL', `ratelimit:${key}`]);
92
+ try {
93
+ await redisPipeline([['DEL', `ratelimit:${key}`]]);
94
+ }
95
+ catch (err) {
96
+ console.error('[actuate][rate-limit] Upstash reset failed:', err instanceof Error ? err.message : err);
97
+ }
66
98
  },
67
99
  };
68
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Best-effort scrubber for secrets pasted into prompts, search queries, and
3
+ * other user-supplied text that ends up in audit logs. Replaces matched
4
+ * patterns with `[REDACTED]` so we don't store live keys/tokens in plain
5
+ * `details` JSON.
6
+ *
7
+ * This is defensive, not exhaustive — clients should never paste secrets
8
+ * into prompts in the first place. The patterns favour false negatives over
9
+ * false positives so we don't mangle legitimate content.
10
+ */
11
+ export declare function redactSecrets(input: string): string;
12
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/security/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOnD"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Best-effort scrubber for secrets pasted into prompts, search queries, and
3
+ * other user-supplied text that ends up in audit logs. Replaces matched
4
+ * patterns with `[REDACTED]` so we don't store live keys/tokens in plain
5
+ * `details` JSON.
6
+ *
7
+ * This is defensive, not exhaustive — clients should never paste secrets
8
+ * into prompts in the first place. The patterns favour false negatives over
9
+ * false positives so we don't mangle legitimate content.
10
+ */
11
+ const PATTERNS = [
12
+ // OpenAI / Anthropic / Google API keys
13
+ { name: 'openai-key', regex: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
14
+ { name: 'anthropic-key', regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
15
+ // Actuate API keys
16
+ { name: 'actuate-key', regex: /\bact_[A-Za-z0-9_]{20,}\b/g },
17
+ // GitHub tokens
18
+ { name: 'github-token', regex: /\bgh[opsu]_[A-Za-z0-9]{30,}\b/g },
19
+ // AWS access keys
20
+ { name: 'aws-access-key', regex: /\bAKIA[0-9A-Z]{16}\b/g },
21
+ // AWS secret access keys (40 char base64-ish, prefixed with common context words)
22
+ { name: 'aws-secret', regex: /\b(?:aws_secret_access_key|aws_secret|secret_key)\s*[:=]\s*["']?([A-Za-z0-9/+]{40})["']?/gi },
23
+ // JWTs (3 base64url segments)
24
+ { name: 'jwt', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
25
+ // Slack tokens
26
+ { name: 'slack-token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
27
+ // Stripe keys
28
+ { name: 'stripe-key', regex: /\b(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}\b/g },
29
+ // Generic password assignment
30
+ { name: 'password-assign', regex: /\b(?:password|passwd|pwd)\s*[:=]\s*["']([^"']{6,})["']/gi },
31
+ ];
32
+ export function redactSecrets(input) {
33
+ if (!input)
34
+ return input;
35
+ let result = input;
36
+ for (const { regex } of PATTERNS) {
37
+ result = result.replace(regex, '[REDACTED]');
38
+ }
39
+ return result;
40
+ }
41
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/security/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,QAAQ,GAA2C;IACvD,uCAAuC;IACvC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,4BAA4B,EAAE;IAC3D,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,gCAAgC,EAAE;IAClE,mBAAmB;IACnB,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,4BAA4B,EAAE;IAC5D,gBAAgB;IAChB,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,gCAAgC,EAAE;IACjE,kBAAkB;IAClB,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,uBAAuB,EAAE;IAC1D,kFAAkF;IAClF,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,4FAA4F,EAAE;IAC3H,8BAA8B;IAC9B,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,oEAAoE,EAAE;IAC5F,eAAe;IACf,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,mCAAmC,EAAE;IACnE,cAAc;IACd,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,kDAAkD,EAAE;IACjF,8BAA8B;IAC9B,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,0DAA0D,EAAE;CAC/F,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,QAAQ,EAAE,CAAC;QACjC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Perform a `fetch()` with SSRF defenses applied:
3
+ *
4
+ * - Reject non-http/https URLs (no `file:`, `gopher:`, `data:`).
5
+ * - Reject hostnames that resolve to private/loopback IP literals
6
+ * (`10.x`, `127.x`, `169.254.x`, `192.168.x`, `::1`, …).
7
+ * - Reject `localhost` and `0.0.0.0`.
8
+ * - Disable HTTP redirects by default (`redirect: 'manual'`) — a 302 to an
9
+ * internal address would otherwise smuggle the request past the validator.
10
+ * - Apply a hard request timeout via `AbortSignal.timeout`.
11
+ *
12
+ * Use this for any internal `fetch()` call that targets a URL derived from
13
+ * user/admin input (webhooks, link health, image URL fetches, etc).
14
+ *
15
+ * Note: this guards against the URL **as written**. For full DNS-rebinding
16
+ * protection use `resolveAndCheck()` from `./webhook.js` to pre-resolve the
17
+ * hostname; on Node 20+ this can be combined with a custom dispatcher to bind
18
+ * the request to the resolved IP. The default policy here trades that
19
+ * protection for portability across runtimes (Edge, Bun, Workers).
20
+ */
21
+ export interface SafeFetchOptions extends RequestInit {
22
+ /** Hard timeout applied via AbortSignal. Defaults to 5000ms. */
23
+ timeoutMs?: number;
24
+ /** When true, allow following redirects. Each hop is re-validated. Default: false. */
25
+ followRedirects?: boolean;
26
+ /** Maximum number of redirect hops when followRedirects is true. Default: 3. */
27
+ maxRedirects?: number;
28
+ }
29
+ export declare class SsrfBlockedError extends Error {
30
+ readonly url: string;
31
+ readonly reason: string;
32
+ constructor(url: string, reason: string);
33
+ }
34
+ export declare function safeFetch(url: string, options?: SafeFetchOptions): Promise<Response>;
35
+ //# sourceMappingURL=safe-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAMxC;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAsCnB"}
@@ -0,0 +1,45 @@
1
+ import { validateWebhookUrl } from './webhook.js';
2
+ export class SsrfBlockedError extends Error {
3
+ url;
4
+ reason;
5
+ constructor(url, reason) {
6
+ super(`SSRF blocked: ${reason} (url=${url})`);
7
+ this.name = 'SsrfBlockedError';
8
+ this.url = url;
9
+ this.reason = reason;
10
+ }
11
+ }
12
+ export async function safeFetch(url, options = {}) {
13
+ const { timeoutMs = 5000, followRedirects = false, maxRedirects = 3, ...init } = options;
14
+ let currentUrl = url;
15
+ let hops = 0;
16
+ while (true) {
17
+ const check = validateWebhookUrl(currentUrl);
18
+ if (!check.valid) {
19
+ throw new SsrfBlockedError(currentUrl, check.error ?? 'URL rejected by SSRF policy');
20
+ }
21
+ const response = await fetch(currentUrl, {
22
+ ...init,
23
+ redirect: 'manual',
24
+ signal: init.signal ?? AbortSignal.timeout(timeoutMs),
25
+ });
26
+ const isRedirect = response.status >= 300 && response.status < 400;
27
+ if (!isRedirect || !followRedirects) {
28
+ return response;
29
+ }
30
+ if (hops >= maxRedirects) {
31
+ throw new SsrfBlockedError(currentUrl, `exceeded ${maxRedirects} redirects`);
32
+ }
33
+ const location = response.headers.get('location');
34
+ if (!location)
35
+ return response;
36
+ try {
37
+ currentUrl = new URL(location, currentUrl).toString();
38
+ }
39
+ catch {
40
+ throw new SsrfBlockedError(location, 'invalid Location header');
41
+ }
42
+ hops += 1;
43
+ }
44
+ }
45
+ //# sourceMappingURL=safe-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AA+BlD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,GAAG,CAAS;IACZ,MAAM,CAAS;IACxB,YAAY,GAAW,EAAE,MAAc;QACrC,KAAK,CAAC,iBAAiB,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAW,EACX,UAA4B,EAAE;IAE9B,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,eAAe,GAAG,KAAK,EAAE,YAAY,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAEzF,IAAI,UAAU,GAAG,GAAG,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,IAAI,gBAAgB,CAAC,UAAU,EAAE,KAAK,CAAC,KAAK,IAAI,6BAA6B,CAAC,CAAC;QACvF,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,EAAE;YACvC,GAAG,IAAI;YACP,QAAQ,EAAE,QAAQ;YAClB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;SACtD,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC;QACnE,IAAI,CAAC,UAAU,IAAI,CAAC,eAAe,EAAE,CAAC;YACpC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,IAAI,IAAI,YAAY,EAAE,CAAC;YACzB,MAAM,IAAI,gBAAgB,CAAC,UAAU,EAAE,YAAY,YAAY,YAAY,CAAC,CAAC;QAC/E,CAAC;QAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE/B,IAAI,CAAC;YACH,UAAU,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,QAAQ,EAAE,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,gBAAgB,CAAC,QAAQ,EAAE,yBAAyB,CAAC,CAAC;QAClE,CAAC;QAED,IAAI,IAAI,CAAC,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Encrypt a value for storage. Returns the original value unchanged when no
3
+ * encryption key is configured (development convenience). Production deployments
4
+ * MUST set `CMS_ENCRYPTION_KEY` — see `security.mdc`.
5
+ */
6
+ export declare function encryptSecret(plaintext: string): Promise<string>;
7
+ /**
8
+ * Decrypt a value that was stored via `encryptSecret`. Plaintext values
9
+ * (written before encryption was enabled, or written by a deployment without
10
+ * the key) are returned unchanged.
11
+ */
12
+ export declare function decryptSecret(stored: string): Promise<string>;
13
+ /** True when the value is stored encrypted (and therefore needs decryption). */
14
+ export declare function isEncrypted(value: string): boolean;
15
+ /**
16
+ * Encrypt each string element in an array. Returns the array unchanged when
17
+ * encryption is disabled. Used for things like TOTP backup codes.
18
+ */
19
+ export declare function encryptStringArray(values: string[]): Promise<string[]>;
20
+ /** Decrypt each element in an array stored via `encryptStringArray`. */
21
+ export declare function decryptStringArray(values: string[]): Promise<string[]>;
22
+ //# sourceMappingURL=secret-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-storage.d.ts","sourceRoot":"","sources":["../../src/security/secret-storage.ts"],"names":[],"mappings":"AAiCA;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMtE;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAWnE;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE5E;AAED,wEAAwE;AACxE,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE5E"}