diff --git a/dispatch.go b/dispatch.go index 87fe01b..5f17695 100644 --- a/dispatch.go +++ b/dispatch.go @@ -45,6 +45,7 @@ func (d *dispatcher) handleRequest(payload []byte) []byte { Query map[string]string `json:"query"` ProjectID string `json:"project_id"` CallerID string `json:"caller_id"` + UserID string `json:"user_id"` CallerRole string `json:"caller_role"` Headers map[string]string `json:"headers"` Body []byte `json:"body"` @@ -70,6 +71,7 @@ func (d *dispatcher) handleRequest(payload []byte) []byte { Body: hr.Body, Caller: CallerIdentity{ CallerID: hr.CallerID, + UserID: hr.UserID, CallerRole: hr.CallerRole, ProjectID: hr.ProjectID, }, diff --git a/native_backends.go b/native_backends.go index 347d069..16a0bf3 100644 --- a/native_backends.go +++ b/native_backends.go @@ -45,6 +45,9 @@ func (b *stubConfigBackend) Get(_ string) (string, bool) { return "", false } // EmitEvent is a no-op outside WASM. func EmitEvent(_ string, _ any) {} +// RecordActivity is a no-op outside WASM. +func RecordActivity(_, _, _, _ string, _ any) {} + // ptrOf and hostError are used by wasm_backends.go (wasip1 only); provide // stubs here so the non-WASM build does not need them. //nolint:unused // used in wasm_backends.go in WASM builds diff --git a/request.go b/request.go index b8a982e..da03e3b 100644 --- a/request.go +++ b/request.go @@ -7,6 +7,9 @@ import "encoding/json" type CallerIdentity struct { // CallerID is the project_member UUID of the caller. CallerID string `json:"caller_id"` + // UserID is the authenticated user's UUID (JWT sub claim). + // Use this as actor_id when recording task activities. + UserID string `json:"user_id"` // CallerRole is the role name of the caller within the project. CallerRole string `json:"caller_role"` // ProjectID is the project the request is scoped to. diff --git a/wasm_backends.go b/wasm_backends.go index 9d77624..8e6e2eb 100644 --- a/wasm_backends.go +++ b/wasm_backends.go @@ -179,6 +179,38 @@ func EmitEvent(topic string, payload any) { ) } +// ── RecordActivity ──────────────────────────────────────────────────────────── + +// activityInput is the JSON shape sent to the paca.activity_record host function. +type activityInput struct { + TaskID string `json:"task_id"` + ProjectID string `json:"project_id"` + ActorID string `json:"actor_id"` + ActivityType string `json:"activity_type"` + Content any `json:"content"` +} + +// RecordActivity appends a task-activity event to the paca activity stream so +// that it is persisted to PostgreSQL by the ActivityConsumer worker. +// actorUserID should be req.Caller.UserID (the authenticated user's UUID). +// content must be JSON-encodable and match the expected shape for activityType. +func RecordActivity(taskID, projectID, actorUserID, activityType string, content any) { + inp := activityInput{ + TaskID: taskID, + ProjectID: projectID, + ActorID: actorUserID, + ActivityType: activityType, + Content: content, + } + payloadBytes, err := json.Marshal(inp) + if err != nil { + return + } + if !hostActivityRecord(int64(ptrOf(payloadBytes)), int64(len(payloadBytes))) { + return + } +} + // ── Helpers ─────────────────────────────────────────────────────────────────── //go:nocheckptr diff --git a/wasm_imports.go b/wasm_imports.go index 3cb4243..8393a94 100644 --- a/wasm_imports.go +++ b/wasm_imports.go @@ -49,6 +49,12 @@ func hostStorageDelete(keyPtr, keyLen int64) int32 //go:noescape func hostEventEmit(topicPtr, topicLen, payloadPtr, payloadLen int64) int32 +// paca.activity_record(payloadPtr i64, payloadLen i64) -> ok i32 +// +//go:wasmimport paca activity_record +//go:noescape +func hostActivityRecord(payloadPtr, payloadLen int64) int32 + // paca.config_get(keyPtr i64, keyLen i64, valuePtrPtr i64, valueLenPtr i64) // //go:wasmimport paca config_get