diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 668987b..2a1d54b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
- dotnet-version: ['8.0.x', '9.0.x']
+ dotnet-version: ['9.0.x']
steps:
- uses: actions/checkout@v4
diff --git a/.gitignore b/.gitignore
index dd701f7..f11b48d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@ bin/
obj/
/packages/
riderModule.iml
+*.DotSettings.user
+*.DotSettings
+/.idea/
/_ReSharper.Caches/.env.test
.env.test
.inngest/
diff --git a/Inngest.Tests/InngestClientAuthBehaviorTests.cs b/Inngest.Tests/InngestClientAuthBehaviorTests.cs
new file mode 100644
index 0000000..795ecc2
--- /dev/null
+++ b/Inngest.Tests/InngestClientAuthBehaviorTests.cs
@@ -0,0 +1,119 @@
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+
+namespace Inngest.Tests;
+
+public class InngestClientAuthBehaviorTests
+{
+ [Fact]
+ public async Task HandleRequestAsync_WhenInngestDevEnvFalse_RequiresSignatureForPut()
+ {
+ var previous = Environment.GetEnvironmentVariable("INNGEST_DEV");
+ try
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", "false");
+
+ var client = new InngestClient(eventKey: "evt", signingKey: "signkey-test-abc123");
+
+ var context = new DefaultHttpContext();
+ context.Request.Method = "PUT";
+ context.Request.Headers.Host = "localhost:5000";
+ context.Request.Scheme = "http";
+ context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"url\":\"http://localhost:5000/api/inngest\"}"));
+ context.Request.ContentLength = context.Request.Body.Length;
+
+ await client.HandleRequestAsync(context);
+
+ Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", previous);
+ }
+ }
+
+ [Fact]
+ public async Task HandleRequestAsync_WhenInngestDevEnvTrue_SkipsSignatureForPut()
+ {
+ var previous = Environment.GetEnvironmentVariable("INNGEST_DEV");
+ try
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", "true");
+
+ var client = new InngestClient(eventKey: "evt", signingKey: "signkey-test-abc123");
+
+ var context = new DefaultHttpContext();
+ context.Request.Method = "PUT";
+ context.Request.Headers.Host = "localhost:5000";
+ context.Request.Scheme = "http";
+ context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{\"url\":\"http://localhost:5000/api/inngest\"}"));
+ context.Request.ContentLength = context.Request.Body.Length;
+
+ await client.HandleRequestAsync(context);
+
+ Assert.NotEqual(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", previous);
+ }
+ }
+
+ [Fact]
+ public async Task HandleRequestAsync_WhenTimestampIsTooFarInFuture_RejectsRequest()
+ {
+ var previous = Environment.GetEnvironmentVariable("INNGEST_DEV");
+ try
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", null);
+
+ const string signingKey = "signkey-test-abc123";
+ var client = new InngestClient(eventKey: "evt", signingKey: signingKey);
+
+ var body = "{\"url\":\"http://localhost:5000/api/inngest\"}";
+ var timestamp = DateTimeOffset.UtcNow.AddMinutes(6).ToUnixTimeSeconds();
+ var signature = ComputeSignature(body, timestamp, signingKey);
+
+ var context = new DefaultHttpContext();
+ context.Request.Method = "PUT";
+ context.Request.Headers.Host = "localhost:5000";
+ context.Request.Scheme = "http";
+ context.Request.Headers["X-Inngest-Signature"] = $"t={timestamp}&s={signature}";
+ context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
+ context.Request.ContentLength = context.Request.Body.Length;
+
+ await client.HandleRequestAsync(context);
+
+ Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("INNGEST_DEV", previous);
+ }
+ }
+
+ private static string ComputeSignature(string body, long timestamp, string signingKey)
+ {
+ var normalizedKey = NormalizeSigningKey(signingKey);
+ var dataToSign = body + timestamp;
+
+ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(normalizedKey));
+ var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
+ return Convert.ToHexString(hashBytes).ToLower();
+ }
+
+ private static string NormalizeSigningKey(string key)
+ {
+ if (string.IsNullOrEmpty(key))
+ return key;
+
+ // Matches SDK regex: ^signkey-\\w+-
+ var idx = key.IndexOf('-', "signkey-".Length);
+ if (!key.StartsWith("signkey-", StringComparison.Ordinal) || idx < 0)
+ return key;
+
+ return key[(idx + 1)..];
+ }
+}
+
diff --git a/Inngest.Tests/StepToolsTests.cs b/Inngest.Tests/StepToolsTests.cs
index ed47ec7..a4964ca 100644
--- a/Inngest.Tests/StepToolsTests.cs
+++ b/Inngest.Tests/StepToolsTests.cs
@@ -112,8 +112,9 @@ public async Task Run_WhenHandlerThrows_ThrowsStepInterruptExceptionWithError()
Assert.Equal("error-step", exception.Operations[0].Id);
Assert.Equal(StepOpCode.StepError, exception.Operations[0].Op);
Assert.NotNull(exception.Operations[0].Error);
- Assert.Equal("InvalidOperationException", exception.Operations[0].Error.Name);
- Assert.Equal("Test error message", exception.Operations[0].Error.Message);
+ var error = exception.Operations[0].Error!;
+ Assert.Equal("InvalidOperationException", error.Name);
+ Assert.Equal("Test error message", error.Message);
}
[Fact]
diff --git a/Inngest/IInngestEvent.cs b/Inngest/IInngestEvent.cs
index 54e56fa..6101194 100644
--- a/Inngest/IInngestEvent.cs
+++ b/Inngest/IInngestEvent.cs
@@ -18,7 +18,7 @@ public interface IInngestEvent
///
/// Custom data payload for the event
///
- public object Data { get; set; }
+ public object? Data { get; set; }
///
/// Timestamp in Unix milliseconds when the event occurred
diff --git a/Inngest/InngestClient.cs b/Inngest/InngestClient.cs
index 37fff44..70ab5d9 100644
--- a/Inngest/InngestClient.cs
+++ b/Inngest/InngestClient.cs
@@ -60,7 +60,10 @@ public InngestClient(
_signingKey = signingKey ?? Environment.GetEnvironmentVariable("INNGEST_SIGNING_KEY") ?? "";
_signingKeyFallback = Environment.GetEnvironmentVariable("INNGEST_SIGNING_KEY_FALLBACK");
_environment = Environment.GetEnvironmentVariable("INNGEST_ENV") ?? "dev";
- _isDev = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("INNGEST_DEV"));
+ var devEnv = Environment.GetEnvironmentVariable("INNGEST_DEV");
+ _isDev = !string.IsNullOrEmpty(devEnv) &&
+ !devEnv.Equals("false", StringComparison.OrdinalIgnoreCase) &&
+ !devEnv.Equals("0", StringComparison.OrdinalIgnoreCase);
_appId = appId ?? Environment.GetEnvironmentVariable("INNGEST_APP_ID") ?? "inngest-dotnet";
// Check for disable cron in dev from environment variable
@@ -71,7 +74,7 @@ public InngestClient(
_logger = logger ?? NullLogger.Instance;
// Set API endpoints based on dev mode and environment variables
- string devServerUrl = Environment.GetEnvironmentVariable("INNGEST_DEV") ?? "http://localhost:8288";
+ string devServerUrl = devEnv ?? "http://localhost:8288";
if (!Uri.TryCreate(devServerUrl, UriKind.Absolute, out _))
{
devServerUrl = "http://localhost:8288";
@@ -310,10 +313,10 @@ public async Task HandleRequestAsync(HttpContext context)
response.Headers["X-Inngest-Sdk"] = $"inngest-dotnet:v{_sdkVersion}";
response.Headers["X-Inngest-Req-Version"] = "1";
- // Verify the request if signatures are required
- if (!_isDev && !await VerifySignature(context))
+ // Verify signature only on requests that carry a body (PUT/POST). GET introspection is allowed to be unauthenticated.
+ if (!_isDev && (request.Method == "PUT" || request.Method == "POST") && !await VerifySignature(context))
{
- response.StatusCode = StatusCodes.Status500InternalServerError;
+ response.StatusCode = StatusCodes.Status401Unauthorized;
await response.WriteAsJsonAsync(new { error = "Invalid signature" });
return;
}
@@ -379,12 +382,12 @@ private async Task VerifySignature(HttpContext context)
return false;
}
- // Verify timestamp is not too old (within last 5 minutes)
+ // Verify timestamp is not too old/new (within last 5 minutes)
var timestampDateTime = DateTimeOffset.FromUnixTimeSeconds(timestamp);
var timeDelta = DateTimeOffset.UtcNow.Subtract(timestampDateTime);
- if (timeDelta.TotalMinutes > 5)
+ if (Math.Abs(timeDelta.TotalMinutes) > 5)
{
- _logger.LogWarning("Signature verification failed: Timestamp too old ({TimeDelta:F1} minutes)", timeDelta.TotalMinutes);
+ _logger.LogWarning("Signature verification failed: Timestamp outside allowed window ({TimeDelta:F1} minutes)", timeDelta.TotalMinutes);
return false;
}
@@ -1467,7 +1470,7 @@ private async Task HandleIntrospectionRequest(HttpContext context)
}
// Always include the unauthenticated response fields
- var responseObj = new Dictionary
+ var responseObj = new Dictionary
{
["function_count"] = _functions.Count,
["has_event_key"] = !string.IsNullOrEmpty(_eventKey),
@@ -1542,4 +1545,4 @@ private static object BuildStepRetryConfig(RetryOptions retry)
return config;
}
-}
\ No newline at end of file
+}
diff --git a/Inngest/InngestEvent.cs b/Inngest/InngestEvent.cs
index 00b3dbc..87f33f0 100644
--- a/Inngest/InngestEvent.cs
+++ b/Inngest/InngestEvent.cs
@@ -28,7 +28,7 @@ public class InngestEvent : IInngestEvent
/// Must be an object, in order to encourage evolving data.
///
[JsonPropertyName("data")]
- public object Data { get; set; } = new();
+ public object? Data { get; set; }
///
/// Timestamp in Unix milliseconds when the event occurred
@@ -61,7 +61,7 @@ public InngestEvent() { }
public InngestEvent(string name, object data)
{
Name = name;
- Data = data;
+ Data = data ?? throw new ArgumentNullException(nameof(data));
Id = Guid.NewGuid().ToString();
}
@@ -86,4 +86,4 @@ public InngestEvent WithIdempotencyKey(string key)
IdempotencyKey = key;
return this;
}
-}
\ No newline at end of file
+}
diff --git a/Inngest/InngestFunctionExtensions.cs b/Inngest/InngestFunctionExtensions.cs
index 3e03f3c..4d4c499 100644
--- a/Inngest/InngestFunctionExtensions.cs
+++ b/Inngest/InngestFunctionExtensions.cs
@@ -28,7 +28,7 @@ public static FunctionDefinition WithStep(this FunctionDefinition function, stri
///
/// The function definition
/// The ID of the step
- /// Duration of the sleep in seconds (informational only)
+ /// Duration of the sleep in seconds (informational only)
/// The updated function definition
public static FunctionDefinition WithSleep(this FunctionDefinition function, string stepId, int durationSeconds)
{
diff --git a/Inngest/Internal/JsonCanonicalizer.cs b/Inngest/Internal/JsonCanonicalizer.cs
index 505d080..250ddae 100644
--- a/Inngest/Internal/JsonCanonicalizer.cs
+++ b/Inngest/Internal/JsonCanonicalizer.cs
@@ -44,7 +44,7 @@ public static string Canonicalize(string json)
/// Canonicalized JSON as UTF-8 bytes
public static byte[] Canonicalize(byte[] jsonBytes)
{
- if (jsonBytes == null || jsonBytes.Length == 0)
+ if (jsonBytes.Length == 0)
return jsonBytes;
try
diff --git a/Inngest/JsonConverters/StringToDictionaryConverter.cs b/Inngest/JsonConverters/StringToDictionaryConverter.cs
index 9a7f0c7..e56c3a2 100644
--- a/Inngest/JsonConverters/StringToDictionaryConverter.cs
+++ b/Inngest/JsonConverters/StringToDictionaryConverter.cs
@@ -8,6 +8,9 @@ namespace Inngest.JsonConverters;
///
public class StringToDictionaryConverter : JsonConverter?>
{
+ ///
+ /// Reads JSON that may be an object, string, or null into a string dictionary.
+ ///
public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
@@ -54,6 +57,9 @@ public class StringToDictionaryConverter : JsonConverter");
}
+ ///
+ /// Writes the dictionary as a JSON object, or null when the value is null.
+ ///
public override void Write(Utf8JsonWriter writer, Dictionary? value, JsonSerializerOptions options)
{
if (value == null)
diff --git a/InngestExample.sln.DotSettings.user b/InngestExample.sln.DotSettings.user
deleted file mode 100644
index 80923ed..0000000
--- a/InngestExample.sln.DotSettings.user
+++ /dev/null
@@ -1,4 +0,0 @@
-
- ForceIncluded
- ForceIncluded
- ForceIncluded
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..ddf9976
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "9.0.301",
+ "rollForward": "latestPatch"
+ }
+}
+