Skip to content
Merged
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
11 changes: 9 additions & 2 deletions openmeter/app/httpdriver/marketplace.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,15 @@ func (h *handler) createBillingProfile(ctx context.Context, installedApp app.App
case app.AppTypeStripe:
return h.makeStripeDefaultBillingApp(ctx, installedApp)
case app.AppTypeSandbox:
// TODO: Implement sandbox billing profile creation
return nil, nil
namespace := installedApp.GetID().Namespace
if err := h.billingService.ProvisionDefaultBillingProfile(ctx, namespace); err != nil {
return nil, fmt.Errorf("provision default billing profile: %w", err)
}
return []api.AppCapabilityType{
api.AppCapabilityType(app.CapabilityTypeCalculateTax),
api.AppCapabilityType(app.CapabilityTypeInvoiceCustomers),
api.AppCapabilityType(app.CapabilityTypeCollectPayments),
}, nil
case app.AppTypeCustomInvoicing:
// TODO: Implement custom invoicing billing profile creation
return nil, nil
Expand Down
20 changes: 18 additions & 2 deletions openmeter/app/sandbox/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"context"
"fmt"

"github.com/samber/lo"

"github.com/openmeterio/openmeter/openmeter/app"
"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/customer"
customerapp "github.com/openmeterio/openmeter/openmeter/customer/app"
"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/models"
)

const (
Expand Down Expand Up @@ -214,12 +217,25 @@ func (a *Factory) NewApp(_ context.Context, appBase app.AppBase) (app.App, error
}, nil
}

func (a *Factory) InstallAppWithAPIKey(ctx context.Context, input app.AppFactoryInstallAppWithAPIKeyInput) (app.App, error) {
// Validate input
func (a *Factory) InstallApp(ctx context.Context, input app.AppFactoryInstallAppInput) (app.App, error) {
if err := input.Validate(); err != nil {
return nil, fmt.Errorf("invalid input: %w", err)
}

// Sandbox is a singleton per namespace — only one instance makes sense since all
// instances are functionally identical (no credentials, no external state).
existing, err := a.appService.ListApps(ctx, app.ListAppInput{
Namespace: input.Namespace,
Type: lo.ToPtr(app.AppTypeSandbox),
})
if err != nil {
return nil, fmt.Errorf("failed to list sandbox apps: %w", err)
}

if existing.TotalCount > 0 {
return nil, models.NewGenericConflictError(fmt.Errorf("sandbox app: %s already exists", existing.Items[0].GetName()))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +225 to +237

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-atomic singleton check (TOCTOU)

The list-then-create pattern is not atomic: two concurrent InstallApp calls that both arrive when no sandbox app exists will both pass the TotalCount > 0 guard and each call CreateApp, producing two sandbox instances despite the comment stating this is a singleton. Because the check and creation are separate DB operations with no unique constraint or advisory lock, the race window is small but real. A database-level unique partial index on (namespace, type) — or wrapping the check and insert in the same transaction — would make the guarantee reliable.

Fix in Claude Code


appBase, err := a.appService.CreateApp(ctx, app.CreateAppInput{
Namespace: input.Namespace,
Name: input.Name,
Expand Down
Loading