This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/sandbox/modules.go
Jeffrey Smith 3d4228f868
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 26s
Feat v0.6.3 dead code sweep (#38)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-31 12:37:47 +00:00

136 lines
4.2 KiB
Go

// Package sandbox — modules.go
//
// Each factory returns a starlarkstruct.Module that the runner
// injects into the script namespace based on granted permissions.
package sandbox
import (
"context"
"fmt"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"switchboard-core/models"
"switchboard-core/store"
)
// ─── Secrets Module ──────────────────────────
//
// Requires permission: secrets.read
// Starlark API:
// val = secrets.get("api_key") → string or None
// all = secrets.list() → list of key names
//
// Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}"
// as a JSON map: {"api_key": "sk-...", "webhook_token": "tok-..."}
// Admin sets them via PUT /admin/extensions/:id/secrets.
// BuildSecretsModule creates the "secrets" module scoped to a package.
func BuildSecretsModule(ctx context.Context, stores store.Stores, packageID string) *starlarkstruct.Module {
// Pre-load secrets for this package (single query, not per-call)
secretMap := loadExtensionSecrets(ctx, stores, packageID)
return MakeModule("secrets", starlark.StringDict{
"get": starlark.NewBuiltin("secrets.get", func(
thread *starlark.Thread, b *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple,
) (starlark.Value, error) {
var key string
if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &key); err != nil {
return nil, err
}
if val, ok := secretMap[key]; ok {
return starlark.String(val), nil
}
return starlark.None, nil
}),
"list": starlark.NewBuiltin("secrets.list", func(
thread *starlark.Thread, b *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple,
) (starlark.Value, error) {
keys := make([]starlark.Value, 0, len(secretMap))
for k := range secretMap {
keys = append(keys, starlark.String(k))
}
return starlark.NewList(keys), nil
}),
})
}
func loadExtensionSecrets(ctx context.Context, stores store.Stores, packageID string) map[string]string {
result := make(map[string]string)
configKey := "ext_secrets:" + packageID
configVal, err := stores.GlobalConfig.Get(ctx, configKey)
if err != nil || configVal == nil {
return result
}
for k, v := range configVal {
if s, ok := v.(string); ok {
result[k] = s
}
}
return result
}
// ─── Notifications Module ────────────────────
//
// Requires permission: notifications.send
// Starlark API:
// notifications.send(user_id, title, body="", type="extension.notify")
//
// Creates an in-app notification via the notification store.
// The extension cannot send email or bypass user preferences —
// that's handled by the notification service layer.
// NotificationSender is the interface the notifications module needs.
// Matches notifications.Service.Notify without importing the package.
type NotificationSender interface {
Notify(ctx context.Context, n *models.Notification) error
}
// BuildNotificationsModule creates the "notifications" module scoped to a package.
func BuildNotificationsModule(ctx context.Context, sender NotificationSender, packageID string) *starlarkstruct.Module {
return MakeModule("notifications", starlark.StringDict{
"send": starlark.NewBuiltin("notifications.send", func(
thread *starlark.Thread, b *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple,
) (starlark.Value, error) {
var userID, title string
var body string
var notifType string = "extension.notify"
if err := starlark.UnpackArgs(b.Name(), args, kwargs,
"user_id", &userID,
"title", &title,
"body?", &body,
"type?", &notifType,
); err != nil {
return nil, err
}
if userID == "" {
return nil, fmt.Errorf("notifications.send: user_id is required")
}
if title == "" {
return nil, fmt.Errorf("notifications.send: title is required")
}
n := &models.Notification{
UserID: userID,
Type: notifType,
Title: title,
Body: body,
ResourceType: "extension",
ResourceID: packageID,
}
if err := sender.Notify(ctx, n); err != nil {
return nil, fmt.Errorf("notifications.send failed: %w", err)
}
return starlark.True, nil
}),
})
}