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
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ members = [
"modules/perf-test",
"modules/module-test",
"modules/environment-test",
"modules/invocation-flags-test",
"templates/basic-rs/spacetimedb",
"templates/chat-console-rs/spacetimedb",
"modules/sdk-test",
Expand Down
24 changes: 24 additions & 0 deletions crates/bindings-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

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

## Function visibility and invocation authentication

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

This library provides a production-ready C++ bindings for SpacetimeDB with complete type system support:
Expand Down
1 change: 1 addition & 0 deletions crates/bindings-cpp/include/spacetimedb/abi/FFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ using ::identity;
// ===== JWT =====
using ::get_jwt;
using ::env_get;
using ::get_call_auth_flags;

// ===== Procedure Transactions =====
using ::procedure_start_mut_tx;
Expand Down
5 changes: 5 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/abi/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

#define STDB_IMPORT_10_6(name) \
__attribute__((import_module("spacetime_10.6"), import_name(#name))) extern
#define STDB_IMPORT_10_7(name) \
__attribute__((import_module("spacetime_10.7"), import_name(#name))) extern

// Import opaque types into global namespace for C compatibility
using SpacetimeDB::Status;
Expand All @@ -64,6 +66,9 @@ extern "C" {

STDB_IMPORT_10_6(env_get)
Status env_get(const uint8_t* key, uint32_t key_len, BytesSource* out);
// Verified invocation authority. Bit 0 is INTERNAL; JWT presence is independent.
STDB_IMPORT_10_7(get_call_auth_flags)
uint32_t get_call_auth_flags();

// ===== Table and Index Management =====
STDB_IMPORT(table_id_from_name)
Expand Down
90 changes: 42 additions & 48 deletions crates/bindings-cpp/include/spacetimedb/auth_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,25 @@ struct ConnectionId;
class AuthCtx {
private:
bool is_internal_;
std::optional<Identity> verified_sender_;
mutable std::shared_ptr<std::optional<JwtClaims>> jwt_;
std::function<std::optional<JwtClaims>()> jwt_loader_;

// Private constructor used by factory methods
AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader);
AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader,
std::optional<Identity> verified_sender = std::nullopt);
static AuthCtx from_connection_with_flags(ConnectionId connection_id, Identity sender, uint32_t flags);

public:
/**
* @brief Creates an AuthCtx from an optional ConnectionId.
*
* If the connection_id is present, creates an AuthCtx that will load the JWT.
* If the connection_id is absent, creates an internal AuthCtx.
* Internal authority is captured from the host, independently of connection presence.
*
* @param connection_id Optional connection ID
* @param sender The identity of the caller (already derived from JWT claims by the host)
* @return An AuthCtx based on the connection_id
* @param sender The verified caller Identity supplied by the host
* @return An AuthCtx with captured invocation authority and lazy JWT loading
*/
static AuthCtx from_connection_id_opt(std::optional<ConnectionId> connection_id, Identity sender);

Expand All @@ -63,11 +66,10 @@ class AuthCtx {
* This is primarily used for testing purposes, allowing you to create
* an AuthCtx with specific JWT claims without needing a real connection.
*
* Note: The Identity must be computed by calling the host function,
* as we cannot compute Blake3 hashes in WASM.
* The Identity must be the verified sender supplied by the host.
*
* @param jwt_payload The raw JWT payload (JSON claims)
* @param identity The identity derived from the JWT's issuer and subject
* @param identity The verified sender Identity
* @return An AuthCtx with the provided JWT
*/
static AuthCtx from_jwt_payload(std::string jwt_payload, Identity identity);
Expand All @@ -76,11 +78,10 @@ class AuthCtx {
* @brief Creates an AuthCtx that reads the JWT for the given connection ID.
*
* The JWT will be lazily loaded from the host when first accessed.
* The identity parameter is the sender's identity, already derived from
* JWT claims by the host (using Blake3 hashing).
* The identity parameter is the verified sender supplied by the host.
*
* @param connection_id The connection ID to load the JWT for
* @param sender The identity of the caller (already derived from JWT claims by the host)
* @param sender The verified sender Identity supplied by the host
* @return An AuthCtx that will load the JWT on demand
*/
static AuthCtx from_connection_id(ConnectionId connection_id, Identity sender);
Expand All @@ -93,9 +94,9 @@ class AuthCtx {
bool is_internal() const { return is_internal_; }

/**
* @brief Checks if there is a JWT without loading it.
* @brief Checks if there is a JWT, loading it lazily if necessary.
*
* If is_internal() returns true, this will return false.
* Independent of is_internal(). Internal calls can also have a JWT.
*
* @return true if a JWT is available
*/
Expand All @@ -113,9 +114,8 @@ class AuthCtx {
/**
* @brief Gets the caller's identity.
*
* For internal calls, this returns the database's identity.
* For external calls, this returns the identity derived from the JWT
* (based on the issuer and subject claims).
* Returns the verified sender captured when constructing the context,
* independently of JWT presence or token claims.
*
* @return The caller's Identity
*/
Expand All @@ -126,16 +126,16 @@ class AuthCtx {
// INLINE IMPLEMENTATIONS
// ============================================================================

constexpr uint16_t ERROR_BUFFER_TOO_SMALL = 11;

inline AuthCtx::AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader)
: is_internal_(is_internal), jwt_loader_(std::move(loader)) {}
inline AuthCtx::AuthCtx(bool is_internal, std::function<std::optional<JwtClaims>()> loader,
std::optional<Identity> verified_sender)
: is_internal_(is_internal), verified_sender_(std::move(verified_sender)), jwt_loader_(std::move(loader)) {}

inline AuthCtx AuthCtx::from_connection_id_opt(std::optional<ConnectionId> connection_id, Identity sender) {
const auto flags = FFI::get_call_auth_flags();
if (connection_id.has_value()) {
return from_connection_id(*connection_id, std::move(sender));
return from_connection_with_flags(*connection_id, std::move(sender), flags);
} else {
return internal();
return AuthCtx((flags & 1) != 0, []() -> std::optional<JwtClaims> { return std::nullopt; }, sender);
}
}

Expand All @@ -144,13 +144,17 @@ inline AuthCtx AuthCtx::internal() {
}

inline AuthCtx AuthCtx::from_jwt_payload(std::string jwt_payload, Identity identity) {
return AuthCtx(false, [payload = std::move(jwt_payload), id = std::move(identity)]() mutable -> std::optional<JwtClaims> {
return AuthCtx(false, [payload = std::move(jwt_payload), id = identity]() mutable -> std::optional<JwtClaims> {
return JwtClaims(std::move(payload), std::move(id));
});
}, identity);
}

inline AuthCtx AuthCtx::from_connection_id(ConnectionId connection_id, Identity sender) {
return AuthCtx(false, [connection_id, sender]() -> std::optional<JwtClaims> {
return from_connection_with_flags(connection_id, std::move(sender), FFI::get_call_auth_flags());
}

inline AuthCtx AuthCtx::from_connection_with_flags(ConnectionId connection_id, Identity sender, uint32_t flags) {
return AuthCtx((flags & 1) != 0, [connection_id, sender]() -> std::optional<JwtClaims> {
// Call the host FFI to get the JWT
BytesSource jwt_source;

Expand All @@ -169,35 +173,24 @@ inline AuthCtx AuthCtx::from_connection_id(ConnectionId connection_id, Identity
}

// Read the JWT payload from the BytesSource
std::vector<uint8_t> buffer;
buffer.resize(4096); // Start with 4KB buffer

size_t buffer_len = buffer.size();
int16_t result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);

while (result == ERROR_BUFFER_TOO_SMALL) {
buffer.resize(buffer.size() * 2);
buffer_len = buffer.size();
result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);
std::array<uint8_t, 4096> buffer;
std::string jwt_payload;
for (;;) {
size_t buffer_len = buffer.size();
const auto result = bytes_source_read(jwt_source, buffer.data(), &buffer_len);
if (result != 0 && result != -1) return std::nullopt;
jwt_payload.append(reinterpret_cast<const char*>(buffer.data()), buffer_len);
// -1 is successful exhaustion and may include the final payload bytes.
if (result == -1) break;
if (buffer_len == 0) return std::nullopt;
}

if (result < 0) {
return std::nullopt;
}

// Convert bytes to string
std::string jwt_payload(buffer.begin(), buffer.begin() + buffer_len);

// Use the provided sender identity (already computed by host from JWT claims)
if (jwt_payload.empty()) return std::nullopt;
// Token claims cannot override the verified sender, including hosted tokens.
return JwtClaims(std::move(jwt_payload), sender);
});
}, sender);
}

inline bool AuthCtx::has_jwt() const {
if (is_internal_) {
return false;
}

// Load the JWT if not already loaded, then check if it has a value
// This ensures has_jwt() and get_jwt() are consistent
return get_jwt().has_value();
Expand All @@ -211,6 +204,7 @@ inline const std::optional<JwtClaims>& AuthCtx::get_jwt() const {
}

inline Identity AuthCtx::get_caller_identity() const {
if (verified_sender_.has_value()) return *verified_sender_;
if (is_internal_) {
// Return database identity for internal calls
std::array<uint8_t, 32> identity_bytes;
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
@@ -1,7 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.

// This was generated using spacetimedb codegen.

#pragma once
#include "../autogen_base.h"

#include <cstdint>
#include <string>
#include <vector>
#include <optional>
#include <memory>
#include "../autogen_base.h"
#include "spacetimedb/bsatn/bsatn.h"

namespace SpacetimeDB::Internal {

SPACETIMEDB_INTERNAL_TAGGED_ENUM(EnvironmentConstraint, std::monostate, std::string, std::vector<std::string>)
}
} // namespace SpacetimeDB::Internal
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.

// This was generated using spacetimedb codegen.

#pragma once

#include <cstdint>
#include <string>
#include <vector>
#include <optional>
#include <memory>
#include "../autogen_base.h"
#include "spacetimedb/bsatn/bsatn.h"
#include "EnvironmentConstraint.g.h"

namespace SpacetimeDB::Internal {

SPACETIMEDB_INTERNAL_PRODUCT_TYPE(EnvironmentDeclaration) {
std::string name;
EnvironmentConstraint constraint;
SpacetimeDB::Internal::EnvironmentConstraint constraint;
bool optional;

void bsatn_serialize(::SpacetimeDB::bsatn::Writer& writer) const {
::SpacetimeDB::bsatn::serialize(writer, name);
::SpacetimeDB::bsatn::serialize(writer, constraint);
::SpacetimeDB::bsatn::serialize(writer, optional);
}
SPACETIMEDB_PRODUCT_TYPE_EQUALITY(name, constraint, optional)
};
}
} // namespace SpacetimeDB::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 @@ -31,5 +31,5 @@

namespace SpacetimeDB::Internal {

SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>, std::vector<SpacetimeDB::Internal::RawSubmoduleV10>, std::vector<SpacetimeDB::Internal::EnvironmentDeclaration>)
SPACETIMEDB_INTERNAL_TAGGED_ENUM(RawModuleDefV10Section, SpacetimeDB::Internal::Typespace, std::vector<SpacetimeDB::Internal::RawTypeDefV10>, std::vector<SpacetimeDB::Internal::RawTableDefV10>, std::vector<SpacetimeDB::Internal::RawReducerDefV10>, std::vector<SpacetimeDB::Internal::RawProcedureDefV10>, std::vector<SpacetimeDB::Internal::RawViewDefV10>, std::vector<SpacetimeDB::Internal::RawScheduleDefV10>, std::vector<SpacetimeDB::Internal::RawLifeCycleReducerDefV10>, std::vector<SpacetimeDB::Internal::RawRowLevelSecurityDefV9>, SpacetimeDB::Internal::CaseConversionPolicy, SpacetimeDB::Internal::ExplicitNames, std::vector<SpacetimeDB::Internal::RawHttpHandlerDefV10>, std::vector<SpacetimeDB::Internal::RawHttpRouteDefV10>, std::vector<SpacetimeDB::Internal::RawViewPrimaryKeyDefV10>, std::vector<SpacetimeDB::Internal::RawSubmoduleV10>, std::vector<SpacetimeDB::Internal::EnvironmentDeclaration>, std::vector<std::string>)
} // 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
Loading
Loading