Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b3d233a
Pin Codefly protobuf generation
AntoineToussaint Jul 23, 2026
6c462af
Expose account credential provisioning contract
AntoineToussaint Jul 23, 2026
5902dd4
Preserve typed self-profile settings in canonical base
AntoineToussaint Jul 23, 2026
65f1bca
Make multi-proto generation deterministic
AntoineToussaint Jul 23, 2026
ff9449a
ci: unblock Codefly gate — publishable agent pins, drop .cache drift,…
AntoineToussaint Jul 24, 2026
5774a23
fix: frontend typecheck + auth-sidecar go.sum so the gate's compile p…
AntoineToussaint Jul 24, 2026
36996b6
review: declare update_mask on profile write; enforce proto-gen deter…
AntoineToussaint Jul 24, 2026
e91c3ee
review: drop cleared profile fields instead of persisting empty strings
AntoineToussaint Jul 24, 2026
a13a0ac
fix: server-side profile merge to remove the self-update read-modify-…
AntoineToussaint Jul 24, 2026
bf495eb
ci: pin Codefly CLI install to the v0.1.27 commit SHA (proto companio…
AntoineToussaint Jul 24, 2026
8e3f19a
ci: install protoc-gen-go/-go-grpc so buf generation works on the runner
AntoineToussaint Jul 24, 2026
0511d8d
ci: pre-seed the fixed proto companion image (0.0.10 -> 0.0.11 content)
AntoineToussaint Jul 24, 2026
fcda9be
ci: build a patched proto companion (0.0.11 image is unpublished)
AntoineToussaint Jul 24, 2026
b7cfe1b
ci: run patched proto companion as the runner UID (fix staged-dir perms)
AntoineToussaint Jul 24, 2026
5a21cf1
ci: give the patched proto companion a clean writable HOME for buf's …
AntoineToussaint Jul 24, 2026
9b9a9c8
ci: pre-create /frontend so the escaping es output can be written non…
AntoineToussaint Jul 24, 2026
456c089
ci: pin protoc-gen-es to v2.11.0 in the patched proto companion
AntoineToussaint Jul 24, 2026
2127c5e
ci: pin fixed agents (go-grpc 0.1.13, nextjs 0.0.115) and drop compan…
AntoineToussaint Jul 25, 2026
31ec7c2
chore: regenerate accounts + auth-sidecar with go-grpc 0.1.13
AntoineToussaint Jul 25, 2026
9b55752
Merge branch 'main' of https://github.com/codefly-dev/module-saas-sta…
AntoineToussaint Jul 25, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ jobs:
env:
GOWORK: "off"
run: go test codefly_sdk_boundary_test.go
- name: Enforce deterministic protocol generation
working-directory: module/tools
env:
GOWORK: "off"
run: go test -run TestMergedProtocolGeneratorsUseOneInvocation .

codefly:
name: Codefly CI
Expand Down
12 changes: 8 additions & 4 deletions module/deployment/topology.bindings.codefly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ services:
agent:
kind: codefly:service
name: go-grpc
version: 0.1.11
version: 0.1.13
publisher: codefly.dev
workspace_configuration_dependencies:
- internal-auth
Expand Down Expand Up @@ -56,6 +56,8 @@ services:
rest-endpoint: true
connect-endpoint: true
with-workspace: true
protocol-output-dirs:
- code/pkg/gen
- name: auth-sidecar
version: 0.0.0
description: |
Expand All @@ -65,7 +67,7 @@ services:
agent:
kind: codefly:service
name: go-grpc
version: 0.1.11
version: 0.1.13
publisher: codefly.dev
workspace_configuration_dependencies:
- gateway
Expand Down Expand Up @@ -95,6 +97,8 @@ services:
spec:
hot-reload: true
rest-endpoint: false
protocol-output-dirs:
- code/pkg/gen
- name: cache
version: 0.0.0
agent:
Expand All @@ -119,7 +123,7 @@ services:
agent:
kind: codefly:service
name: nextjs
version: 0.0.114
version: 0.0.115
publisher: codefly.dev
workspace_configuration_dependencies:
- workos
Expand Down Expand Up @@ -160,7 +164,7 @@ services:
agent:
kind: codefly:service
name: postgres
version: 0.0.103
version: 0.0.104
publisher: codefly.dev
endpoints:
- name: tcp
Expand Down
6 changes: 5 additions & 1 deletion module/services/accounts/code/pkg/business/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ func (s *Service) UpdateUser(ctx context.Context, actorID string, access Identit
updates["primary_email"] = req.User.PrimaryEmail
}
if req.User.Profile != nil {
updates["profile"] = req.User.Profile
// Merge the profile map (set non-empty keys, delete blanked keys,
// preserve the rest) so callers send only the fields they changed
// and don't have to read-modify-write the whole map. GDPR's
// anonymization uses the "profile" replace path directly.
updates["profile_merge"] = req.User.Profile
}
}

Expand Down

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

50 changes: 50 additions & 0 deletions module/services/accounts/code/pkg/infra/postgres_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,59 @@ func (s *PostgresStore) UpdateUser(ctx context.Context, userID string, updates m
}
}

if patch, ok := updates["profile_merge"]; ok {
if err := s.mergeUserProfile(ctx, executor, userID, patch); err != nil {
return nil, w.Wrap(err)
}
}

return s.GetUser(ctx, userID)
}

// mergeUserProfile applies a partial profile patch atomically: keys with a
// non-empty value are set, keys with an empty value are removed, and every
// other existing key is preserved. The row is locked FOR UPDATE so concurrent
// profile writers serialize on the server instead of a caller having to
// read-modify-write the whole map (which loses concurrent changes).
//
// This is deliberately separate from the "profile" replace path, which GDPR
// anonymization (business.(*Service).processDeletion) relies on to scrub PII
// by overwriting the entire map — a merge there would preserve the PII.
func (s *PostgresStore) mergeUserProfile(ctx context.Context, executor QueryExecutor, userID string, patch any) error {
w := wool.Get(ctx).In("mergeUserProfile")
fields, ok := patch.(map[string]string)
if !ok {
return w.NewError("profile_merge expects map[string]string, got %T", patch)
}
var raw []byte
if err := executor.QueryRow(ctx,
`SELECT profile FROM users WHERE uuid = $1 FOR UPDATE`, userID).Scan(&raw); err != nil {
return w.Wrapf(err, "failed to read profile for merge")
}
current := map[string]string{}
if len(raw) > 0 {
if err := json.Unmarshal(raw, &current); err != nil {
return w.Wrapf(err, "malformed profile json")
}
}
for key, value := range fields {
if value == "" {
delete(current, key)
} else {
current[key] = value
}
}
merged, err := json.Marshal(current)
if err != nil {
return w.Wrapf(err, "failed to marshal merged profile")
}
if _, err := executor.Exec(ctx,
`UPDATE users SET profile = $1, updated_at = CURRENT_TIMESTAMP WHERE uuid = $2`, merged, userID); err != nil {
return w.Wrapf(err, "failed to update profile")
}
return nil
}

// DeleteUser soft-deletes a user by setting status to 'deleted'.
func (s *PostgresStore) DeleteUser(ctx context.Context, userID string) error {
w := wool.Get(ctx).In("DeleteUser")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package infra_test

import (
"context"
"testing"

"accounts/pkg/business"

"github.com/stretchr/testify/require"
)

// TestUpdateUserProfileMerge verifies the profile_merge path: non-empty keys are
// set, blank keys are removed, and untouched keys are preserved — so a caller can
// send only the fields it changed without a read-modify-write of the whole map.
func TestUpdateUserProfileMerge(t *testing.T) {
id := seedUser(t)
require.NoError(t, testStore.As(business.Identity{UserID: id}).Within(testCtx, func(ctx context.Context) error {
if _, err := testStore.UpdateUser(ctx, id, map[string]any{
"profile_merge": map[string]string{"name": "Ada", "title": "Engineer", "phone": "555"},
}); err != nil {
return err
}

// Update name, clear title (blank), add bio, leave phone untouched.
user, err := testStore.UpdateUser(ctx, id, map[string]any{
"profile_merge": map[string]string{"name": "Ada Lovelace", "title": "", "bio": "hello"},
})
if err != nil {
return err
}

require.Equal(t, map[string]string{
"name": "Ada Lovelace",
"phone": "555",
"bio": "hello",
}, user.Profile)
return nil
}))
}
18 changes: 10 additions & 8 deletions module/services/accounts/proto/buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,29 @@ managed:
- file_option: go_package_prefix
value: accounts/pkg/gen
plugins:
# Pin every remote plugin. Codefly owns invocation; this template owns the
# byte-generating toolchain so a registry release cannot change a CI build.
- remote: buf.build/protocolbuffers/go:v1.36.11
# Codefly's versioned proto companion owns the byte-generating toolchain.
# Local plugins avoid mutable registry execution and anonymous rate limits.
- local: protoc-gen-go
out: ../code/pkg/gen
opt: paths=source_relative
- remote: buf.build/grpc/go:v1.6.1
- local: protoc-gen-go-grpc
out: ../code/pkg/gen
opt: paths=source_relative
- remote: buf.build/grpc-ecosystem/gateway:v2.29.0
- local: protoc-gen-grpc-gateway
out: ../code/pkg/gen
opt: paths=source_relative
- remote: buf.build/connectrpc/go:v1.20.0
- local: protoc-gen-connect-go
out: ../code/pkg/gen
opt: paths=source_relative
- remote: buf.build/grpc-ecosystem/openapiv2:v2.29.0
- local: protoc-gen-openapiv2
out: ../.cache/openapi
strategy: all
opt:
- allow_merge=true
- merge_file_name=api
- remote: buf.build/bufbuild/es:v2.11.0
- local: protoc-gen-es
out: ../../frontend/code/src/gen
strategy: all
include_imports: true
opt:
- target=ts
Expand Down
76 changes: 72 additions & 4 deletions module/services/accounts/proto/codefly/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,55 @@ package saas.accounts.v1;
// Codefly currently advertises a service's gRPC dependency contract from the
// proto/codefly/api.proto convention. Accounts' canonical, catalog-complete
// schema is split under saas/accounts/v1; this deliberately narrow facade keeps
// the internal identity boundary available to generated service clients without
// duplicating these symbols in Accounts' own Buf generation (buf.yaml excludes
// this file). Keep field numbers and wire method names aligned with the canonical
// identity.proto and api_keys.proto definitions.
// identity enforcement plus real dependency-test credential provisioning
// available to generated service clients without duplicating these symbols in
// Accounts' own Buf generation (buf.yaml excludes this file). Keep field
// numbers and wire method names aligned with the canonical schemas.

message FixtureAuthentication {
string token = 1;
}

message AuthenticateRequest {
string provider = 1;
string provider_id = 2 [deprecated = true];
string provider_email = 3 [deprecated = true];
bool email_verified = 4 [deprecated = true];
map<string, string> profile = 5;
string device_info = 6;
oneof authentication {
FixtureAuthentication fixture = 8;
}
}

message User {
string uuid = 1;
}

message AuthenticateResponse {
string access_token = 1;
string refresh_token = 2;
int64 expires_in = 3;
User user = 4;
}

service AuthService {
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse);
}

message ListOrganizationsRequest {}

message Organization {
string id = 1;
}

message ListOrganizationsResponse {
repeated Organization organizations = 1;
}

service OrganizationService {
rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse);
}

message ResolveIdentityRequest {
string provider = 1;
Expand Down Expand Up @@ -45,6 +90,29 @@ message ValidateAPIKeyResponse {
string principal_kind = 8;
}

enum APIKeyEnvironment {
API_KEY_ENVIRONMENT_UNSPECIFIED = 0;
API_KEY_ENVIRONMENT_LIVE = 1;
API_KEY_ENVIRONMENT_TEST = 2;
}

message Permission {
string resource = 1;
string action = 2;
}

message CreateAPIKeyRequest {
string organization_id = 1;
string name = 2;
repeated Permission scopes = 3;
APIKeyEnvironment environment = 4;
}

message CreateAPIKeyResponse {
string plaintext_key = 2;
}

service APIKeyService {
rpc CreateAPIKey(CreateAPIKeyRequest) returns (CreateAPIKeyResponse);
rpc ValidateAPIKey(ValidateAPIKeyRequest) returns (ValidateAPIKeyResponse);
}
4 changes: 3 additions & 1 deletion module/services/accounts/service.codefly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version: 0.0.0
agent:
kind: codefly:service
name: go-grpc
version: 0.1.11
version: 0.1.13
publisher: codefly.dev
service-dependencies:
- name: cache
Expand All @@ -31,5 +31,7 @@ endpoints:
spec:
connect-endpoint: true
hot-reload: true
protocol-output-dirs:
- code/pkg/gen
rest-endpoint: true
with-workspace: true
Loading
Loading