Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions crates/bindings-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@

The SpacetimeDB C++ Module Library provides a modern C++20 API for building SpacetimeDB modules that run inside the database as WebAssembly.

## Invocation authentication
## Function visibility and invocation authentication

`ctx.sender_auth().is_internal()` captures host-verified invocation authority. JWT identity is the verified sender supplied by the host. Procedures preserve authentication in `with_tx` and `try_with_tx`. Internal authority is independent of connection and JWT presence. Newly compiled modules advertise `hosted_auth_v1` and require a compatible host. Function visibility and scheduled defaults are unchanged.
Apply `SPACETIMEDB_FUNCTION_VISIBILITY(name, Public)`, `Private`, or `Internal`
to a reducer or procedure after its definition:

```cpp
SPACETIMEDB_REDUCER(process_jobs, ReducerContext ctx) {
return Ok();
}
SPACETIMEDB_FUNCTION_VISIBILITY(process_jobs, Internal);
```

Omission means public for ordinary functions and private for scheduled functions.
An explicit choice is preserved when the function is scheduled. Lifecycle
reducers permit only omission or `Internal` and can only run for their host
lifecycle event. Internal functions require verified internal authority. Private
functions also admit the owner, and public functions admit any client.

`ctx.sender_auth().is_internal()` captures the host's invocation authority. It is
independent of connection and JWT presence, so an internal call can have a JWT.
JWT identity is the verified sender supplied by the host. Procedures preserve
this authentication in `with_tx` and `try_with_tx`. Newly compiled modules emit
schema V10 and advertise `hosted_auth_v1`, requiring a compatible host.

## Current State

Expand Down
6 changes: 6 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/function_visibility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

namespace SpacetimeDB {
// Omission preserves the host default: Public ordinarily, Private when scheduled.
enum class FunctionVisibility { Public, Private, Internal };
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ namespace SpacetimeDB::Internal {
enum class FunctionVisibility : uint8_t {
Private = 0,
ClientCallable = 1,
Internal = 2,
ExplicitClientCallable = 3,
};
} // namespace SpacetimeDB::Internal
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <cstdio>
#include "../bsatn/bsatn.h"
#include "../database.h"
#include "../function_visibility.h"
#include "autogen/CaseConversionPolicy.g.h"
#include "autogen/ExplicitNameEntry.g.h"
#include "autogen/NameMapping.g.h"
Expand Down Expand Up @@ -49,6 +50,7 @@ void fail_reducer(std::string message);

namespace Internal {

// Builds the V10 module definition with explicit function visibility.
class V10Builder {
public:
V10Builder() = default;
Expand Down Expand Up @@ -437,7 +439,7 @@ class V10Builder {
RawReducerDefV10 reducer_def{
reducer_name,
ProductType{},
FunctionVisibility::Private,
FunctionVisibility::Internal,
MakeUnitAlgebraicType(),
MakeStringAlgebraicType(),
};
Expand Down Expand Up @@ -646,6 +648,7 @@ class V10Builder {

void RegisterExplicitTableName(const std::string& source_name, const std::string& canonical_name);
void RegisterExplicitFunctionName(const std::string& source_name, const std::string& canonical_name);
void SetFunctionVisibility(const std::string& source_name, ::SpacetimeDB::FunctionVisibility visibility);
void RegisterExplicitIndexName(const std::string& source_name, const std::string& canonical_name);

RawModuleDefV10 BuildModuleDef() const;
Expand Down
10 changes: 9 additions & 1 deletion crates/bindings-cpp/include/spacetimedb/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,15 @@ inline std::vector<std::string> parseParameterNames(const std::string& param_lis
// VISIBILITY FILTER MACRO
// =============================================================================

// Apply to a registered reducer or procedure. Runs after function registration;
// lifecycle reducers only accept Internal. Scheduling preserves this choice.
#define SPACETIMEDB_FUNCTION_VISIBILITY(function_name, visibility) \
extern "C" __attribute__((export_name("__preinit__40_visibility_" #function_name))) \
void CONCAT(__spacetimedb_function_visibility_, function_name)() { \
::SpacetimeDB::Internal::getV10Builder().SetFunctionVisibility( \
#function_name, ::SpacetimeDB::FunctionVisibility::visibility); \
}

/**
* @brief Set module case conversion policy using a fixed preinit registration symbol.
*
Expand Down Expand Up @@ -917,4 +926,3 @@ inline std::vector<std::string> parseParameterNames(const std::string& param_lis


#endif // SPACETIMEDB_MACROS_H

43 changes: 25 additions & 18 deletions crates/bindings-cpp/src/internal/v10_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,31 @@ RawConstraintDefV10 V10Builder::CreateUniqueConstraint(const std::string& table_
};
}

void V10Builder::SetFunctionVisibility(const std::string& name, ::SpacetimeDB::FunctionVisibility visibility) {
FunctionVisibility declared;
switch (visibility) {
case ::SpacetimeDB::FunctionVisibility::Public: declared = FunctionVisibility::ExplicitClientCallable; break;
case ::SpacetimeDB::FunctionVisibility::Private: declared = FunctionVisibility::Private; break;
case ::SpacetimeDB::FunctionVisibility::Internal: declared = FunctionVisibility::Internal; break;
default:
SetConstraintRegistrationError("INVALID_FUNCTION_VISIBILITY", "function='" + name + "'");
return;
}
for (const auto& lifecycle : lifecycle_reducers_) {
if (lifecycle.function_name == name && declared != FunctionVisibility::Internal) {
SetConstraintRegistrationError("INVALID_LIFECYCLE_VISIBILITY", "function='" + name + "' must be Internal");
return;
}
}
for (auto& reducer : reducers_) {
if (reducer.source_name == name) { reducer.visibility = declared; return; }
}
for (auto& procedure : procedures_) {
if (procedure.source_name == name) { procedure.visibility = declared; return; }
}
SetConstraintRegistrationError("UNKNOWN_FUNCTION_VISIBILITY", "function='" + name + "' is not a reducer or procedure");
}

RawModuleDefV10 V10Builder::BuildModuleDef() const {
RawModuleDefV10 v10_module;

Expand All @@ -228,24 +253,6 @@ RawModuleDefV10 V10Builder::BuildModuleDef() const {
std::vector<RawReducerDefV10> reducers = reducers_;
std::vector<RawProcedureDefV10> procedures = procedures_;

std::unordered_set<std::string> internal_functions;
for (const auto& lifecycle : lifecycle_reducers_) {
internal_functions.insert(lifecycle.function_name);
}
for (const auto& schedule : schedules_) {
internal_functions.insert(schedule.function_name);
}
for (auto& reducer : reducers) {
if (internal_functions.find(reducer.source_name) != internal_functions.end()) {
reducer.visibility = FunctionVisibility::Private;
}
}
for (auto& procedure : procedures) {
if (internal_functions.find(procedure.source_name) != internal_functions.end()) {
procedure.visibility = FunctionVisibility::Private;
}
}

RawModuleDefV10Section section_typespace;
section_typespace.set<0>(typespace_);
v10_module.sections.push_back(section_typespace);
Expand Down
13 changes: 12 additions & 1 deletion crates/bindings-cpp/tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
endif()

add_executable(bindings_cpp_unit_tests
hosted_auth_unit_tests.cpp
main.cpp
http_unit_tests.cpp
environment_unit_tests.cpp
hosted_auth_unit_tests.cpp
function_visibility_unit_tests.cpp
)

# Exercise the real module builder without the standalone WASI shims, which
# replace the Node test runner's standard I/O and process lifecycle functions.
target_sources(bindings_cpp_unit_tests PRIVATE
../../src/internal/Module.cpp
../../src/internal/AlgebraicType.cpp
../../src/internal/v9_builder.cpp
../../src/internal/v10_builder.cpp
../../src/internal/module_type_registration.cpp
)

target_include_directories(bindings_cpp_unit_tests PRIVATE
Expand Down
115 changes: 115 additions & 0 deletions crates/bindings-cpp/tests/unit/function_visibility_unit_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#include "test_harness.h"
#include "spacetimedb/reducer_error.h"
#include "spacetimedb/procedure_context.h"
#include "spacetimedb/internal/v10_builder.h"
#include "spacetimedb/internal/autogen/RawModuleDef.g.h"
#include "spacetimedb/macros.h"

using namespace SpacetimeDB;
using namespace SpacetimeDB::Internal;

namespace {
ReducerResult noop(ReducerContext) { return Ok(); }
uint32_t procedure(ProcedureContext) { return 7; }
}

SPACETIMEDB_FUNCTION_VISIBILITY(visibility_macro_target, Internal);

TEST_CASE(visibility_macro_applies_after_function_registration) {
auto& builder = getV10Builder();
builder.RegisterReducer("visibility_macro_target", &noop, {});
__spacetimedb_function_visibility_visibility_macro_target();
bool found = false;
for (const auto& section : builder.BuildModuleDef().sections) {
if (section.get_tag() != 3) continue;
for (const auto& reducer : section.get<3>()) {
if (reducer.source_name != "visibility_macro_target") continue;
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::Internal, reducer.visibility);
found = true;
}
}
ASSERT_TRUE(found);
}

TEST_CASE(v10_retains_explicit_visibility_and_schedule_default) {
V10Builder builder;
builder.RegisterReducer("omitted", &noop, {});
builder.RegisterReducer("public", &noop, {});
builder.RegisterReducer("private", &noop, {});
builder.RegisterReducer("internal", &noop, {});
builder.SetFunctionVisibility("public", SpacetimeDB::FunctionVisibility::Public);
builder.SetFunctionVisibility("private", SpacetimeDB::FunctionVisibility::Private);
builder.SetFunctionVisibility("internal", SpacetimeDB::FunctionVisibility::Internal);
builder.RegisterSchedule("jobs", 0, "public");
builder.RegisterSchedule("other_jobs", 0, "omitted");
builder.RegisterProcedure("procedure", &procedure);
builder.SetFunctionVisibility("procedure", SpacetimeDB::FunctionVisibility::Internal);

RawModuleDef versioned;
versioned.set<2>(builder.BuildModuleDef());
std::vector<uint8_t> bytes;
bsatn::Writer writer(bytes);
bsatn::serialize(writer, versioned);
ASSERT_EQ(uint8_t{2}, bytes.at(0));
ASSERT_EQ(uint8_t{2}, versioned.get_tag());
bool saw_reducers = false, saw_procedure = false, saw_environment = false, saw_capability = false;
for (const auto& section : versioned.get<2>().sections) {
if (section.get_tag() == 3) {
const auto& reducers = section.get<3>();
ASSERT_EQ(size_t{4}, reducers.size());
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::ClientCallable, reducers[0].visibility);
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::ExplicitClientCallable, reducers[1].visibility);
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::Private, reducers[2].visibility);
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::Internal, reducers[3].visibility);
saw_reducers = true;
} else if (section.get_tag() == 4) {
ASSERT_EQ(SpacetimeDB::Internal::FunctionVisibility::Internal, section.get<4>().at(0).visibility);
saw_procedure = true;
} else if (section.get_tag() == 15) {
ASSERT_TRUE(section.get<15>().empty());
saw_environment = true;
} else if (section.get_tag() == 16) {
ASSERT_EQ(std::vector<std::string>{"hosted_auth_v1"}, section.get<16>());
saw_capability = true;
}
}
ASSERT_TRUE(saw_reducers && saw_procedure && saw_environment && saw_capability);
}

TEST_CASE(v10_visibility_extends_enum_without_changing_reducer_field_layout) {
V10Builder builder;
builder.RegisterReducer("r", &noop, {});
auto reducer = builder.GetReducers().at(0);
for (uint8_t tag = 0; tag <= 3; ++tag) {
reducer.visibility = static_cast<SpacetimeDB::Internal::FunctionVisibility>(tag);
std::vector<uint8_t> bytes;
bsatn::Writer writer(bytes);
bsatn::serialize(writer, reducer);
const std::vector<uint8_t> expected{1, 0, 0, 0, 'r', 0, 0, 0, 0, tag, 2, 0, 0, 0, 0, 4};
ASSERT_EQ(expected, bytes);
const RawProcedureDefV10 procedure_def{
"p", ProductType{}, reducer.ok_return_type, reducer.visibility,
};
std::vector<uint8_t> procedure_bytes;
bsatn::Writer procedure_writer(procedure_bytes);
bsatn::serialize(procedure_writer, procedure_def);
const std::vector<uint8_t> expected_procedure{
1, 0, 0, 0, 'p', 0, 0, 0, 0, 2, 0, 0, 0, 0, tag,
};
ASSERT_EQ(expected_procedure, procedure_bytes);
}
}

TEST_CASE(v10_environment_and_capabilities_have_distinct_appended_wire_tags) {
RawModuleDefV10Section environment;
environment.set<15>(std::vector<EnvironmentDeclaration>{});
RawModuleDefV10Section capabilities;
capabilities.set<16>(std::vector<std::string>{});
for (const auto& section : {environment, capabilities}) {
std::vector<uint8_t> bytes;
bsatn::Writer writer(bytes);
bsatn::serialize(writer, section);
const std::vector<uint8_t> expected{section.get_tag(), 0, 0, 0, 0};
ASSERT_EQ(expected, bytes);
}
}
60 changes: 60 additions & 0 deletions crates/bindings-csharp/Codegen.Tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,66 @@ public static void @params(ProcedureContext ctx)
Assert.Empty(GetCompilationErrors(compilationAfterGen));
}

[Fact]
public static async Task ExplicitFunctionVisibilityCompilesAndRejectsExternalLifecycle()
{
var fixture = await Fixture.Compile("server");
const string source = """
using SpacetimeDB;
public static partial class VisibilityFunctions
{
[Reducer(Visibility = FunctionVisibility.Public)]
public static void PublicJob(ReducerContext ctx) {}
[Reducer(Visibility = FunctionVisibility.Private)]
public static void PrivateJob(ReducerContext ctx) {}
[Reducer(Visibility = FunctionVisibility.Internal)]
public static void InternalJob(ReducerContext ctx) {}
[Procedure(Visibility = FunctionVisibility.Internal)]
public static int InternalProcedure(ProcedureContext ctx) => 1;
}
""";
var parseOptions = fixture.ParseOptions;
var tree = CSharpSyntaxTree.ParseText(source, parseOptions);
var compilation = fixture.SampleCompilation.AddSyntaxTrees(tree);
var driver = CSharpGeneratorDriver.Create(
[
new SpacetimeDB.Codegen.Type().AsSourceGenerator(),
new SpacetimeDB.Codegen.Module().AsSourceGenerator(),
new EnvironmentGenerator().AsSourceGenerator(),
],
parseOptions: parseOptions
);
var result = driver.RunGenerators(compilation).GetRunResult();
Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
Assert.Empty(GetCompilationErrors(compilation.AddSyntaxTrees(result.GeneratedTrees)));
var generated = string.Join("\n", result.GeneratedTrees.Select(t => t.ToString()));
Assert.Contains(
"Visibility: SpacetimeDB.Internal.FunctionVisibility.ExplicitClientCallable",
generated
);
Assert.Contains("Visibility: SpacetimeDB.Internal.FunctionVisibility.Private", generated);
Assert.Contains("Visibility: SpacetimeDB.Internal.FunctionVisibility.Internal", generated);

var invalid = CSharpSyntaxTree.ParseText(
"""
using SpacetimeDB;
public static partial class BadVisibility
{
[Reducer(ReducerKind.Init, Visibility = FunctionVisibility.Public)]
public static void InvalidLifecycle(ReducerContext ctx) {}
}
""",
parseOptions
);
var rejected = driver
.RunGenerators(fixture.SampleCompilation.AddSyntaxTrees(invalid))
.GetRunResult();
Assert.Contains(
rejected.Diagnostics,
diagnostic => diagnostic.GetMessage().Contains("Lifecycle reducers only permit")
);
}

[Fact]
public static async Task TestDiagnostics()
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2348,7 +2348,7 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar
new(
SourceName: nameof(Init),
Params: [],
Visibility: SpacetimeDB.Internal.FunctionVisibility.ClientCallable,
Visibility: SpacetimeDB.Internal.FunctionVisibility.Internal,
OkReturnType: SpacetimeDB.BSATN.AlgebraicType.Unit,
ErrReturnType: new SpacetimeDB.BSATN.AlgebraicType.String(default)
);
Expand Down
Loading
Loading