diff --git a/Jurassic.4.0.0.nupkg b/Jurassic.4.0.0.nupkg
new file mode 100644
index 000000000..d9434a9de
Binary files /dev/null and b/Jurassic.4.0.0.nupkg differ
diff --git a/nuget.config b/nuget.config
index b1738fc41..da7c4d0ca 100644
--- a/nuget.config
+++ b/nuget.config
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/nuget.exe b/nuget.exe
new file mode 100644
index 000000000..ec1309c7a
Binary files /dev/null and b/nuget.exe differ
diff --git a/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg b/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg
new file mode 100644
index 000000000..d9434a9de
Binary files /dev/null and b/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg differ
diff --git a/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg.sha512 b/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg.sha512
new file mode 100644
index 000000000..369254cf8
--- /dev/null
+++ b/packages/jurassic/4.0.0/jurassic.4.0.0.nupkg.sha512
@@ -0,0 +1 @@
+p41pr8YRXXiqrW5DrLpCxbsyTZwZ9ONcdFjYaHorvCuvPeMiBgl+WOFKkzLENEnJdh22tkh7FKjfSMW1S1jtZg==
\ No newline at end of file
diff --git a/packages/jurassic/4.0.0/jurassic.nuspec b/packages/jurassic/4.0.0/jurassic.nuspec
new file mode 100644
index 000000000..feb71c615
--- /dev/null
+++ b/packages/jurassic/4.0.0/jurassic.nuspec
@@ -0,0 +1,16 @@
+
+
+
+ Jurassic
+ 4.0.0
+ Paul Bartrum
+ Paul Bartrum
+ false
+ A .NET library to parse and execute JavaScript code.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs
new file mode 100644
index 000000000..f137d66e9
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs
@@ -0,0 +1,20 @@
+// ==========================================================================
+// IScriptEngine.cs
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex Group
+// All rights reserved.
+// ==========================================================================
+
+using System.Threading.Tasks;
+using Squidex.Domain.Apps.Core.Contents;
+
+namespace Squidex.Domain.Apps.Core.Scripting
+{
+ public interface IScriptEngine
+ {
+ Task ExecuteAsync(ScriptContext context, string operationName, string script);
+
+ Task ExecuteAndTransformAsync(ScriptContext context, string operationName, string script);
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs b/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs
new file mode 100644
index 000000000..105614039
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs
@@ -0,0 +1,103 @@
+// ==========================================================================
+// JurassicScriptEngine.cs
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Security;
+using System.Threading.Tasks;
+using Jurassic;
+using Jurassic.Library;
+using Newtonsoft.Json;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Tasks;
+
+// ReSharper disable ConvertToLambdaExpression
+
+namespace Squidex.Domain.Apps.Core.Scripting
+{
+ public sealed class JurassicScriptEngine : IScriptEngine
+ {
+ private readonly JsonSerializerSettings serializerSettings;
+
+ public JurassicScriptEngine(JsonSerializerSettings serializerSettings)
+ {
+ Guard.NotNull(serializerSettings, nameof(serializerSettings));
+
+ this.serializerSettings = serializerSettings;
+ }
+
+ public Task ExecuteAsync(ScriptContext context, string operationName, string script)
+ {
+ Guard.NotNull(context, nameof(context));
+
+ if (!string.IsNullOrWhiteSpace(script))
+ {
+ return TaskHelper.False;
+ }
+
+ var engine = CreateScriptEngine(context, operationName);
+
+ engine.Execute(script);
+
+ return TaskHelper.False;
+ }
+
+ public Task ExecuteAndTransformAsync(ScriptContext context, string operationName, string script)
+ {
+ Guard.NotNull(context, nameof(context));
+
+ if (!string.IsNullOrWhiteSpace(script))
+ {
+ return Task.FromResult(context.Data);
+ }
+
+ var result = context.Data;
+
+ var engine = CreateScriptEngine(context, operationName);
+
+ engine.SetGlobalFunction("replace", new Action(data =>
+ {
+ try
+ {
+ result = JsonConvert.DeserializeObject(JSONObject.Stringify(engine, data));
+ }
+ catch
+ {
+ result = new NamedContentData();
+ }
+ }));
+
+ engine.Execute(script);
+
+ return Task.FromResult(result);
+ }
+
+ private ScriptEngine CreateScriptEngine(ScriptContext context, string operationName)
+ {
+ Guard.NotNullOrEmpty(operationName, nameof(operationName));
+
+ var engine = new ScriptEngine();
+
+ engine.SetGlobalFunction("disallow", new Action(message =>
+ {
+ throw new SecurityException(message);
+ }));
+
+ engine.SetGlobalFunction("reject", new Action(message =>
+ {
+ throw new ValidationException($"Failed to '{operationName}", !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null);
+ }));
+
+ var json = JsonConvert.SerializeObject(context, serializerSettings);
+
+ engine.SetGlobalValue("ctx", JSONObject.Parse(engine, json));
+
+ return engine;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs
new file mode 100644
index 000000000..fb21649a4
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// ScriptContext.cs
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using Squidex.Domain.Apps.Core.Contents;
+
+namespace Squidex.Domain.Apps.Core.Scripting
+{
+ public sealed class ScriptContext
+ {
+ public ScriptUser User { get; set; }
+
+ public Guid ContentId { get; set; }
+
+ public NamedContentData Data { get; set; }
+
+ public NamedContentData OldData { get; set; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs b/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs
new file mode 100644
index 000000000..d0a25d3f3
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+
+namespace Squidex.Domain.Apps.Core.Scripting
+{
+ public sealed class ScriptUser
+ {
+ public string Id { get; set; }
+
+ public string Email { get; set; }
+
+ public bool IsClient { get; set; }
+
+ public Dictionary Scopes { get; } = new Dictionary();
+
+ public static ScriptUser Create(ClaimsPrincipal principal)
+ {
+ return new ScriptUser();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj b/src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj
index dff5093fa..c811f6da4 100644
--- a/src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj
+++ b/src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj
@@ -10,7 +10,9 @@
+
+