// Package sandbox — connections_module.go // // Requires permission: connections.read // // Starlark API: // conn = connections.get("gitea") → dict or None // conn = connections.get("gitea", name="Work Gitea") → dict or None // all = connections.list("gitea") → list of dicts // // Each returned dict contains the connection's config fields with // secrets decrypted. The module delegates to a ConnectionResolver // interface so the runner stays agnostic to the vault. package sandbox import ( "context" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" "switchboard-core/models" ) // ConnectionResolver resolves extension connections with secret decryption. // Implemented by the handler layer which owns the vault. type ConnectionResolver interface { // Resolve returns a single connection's decrypted config via the // scope chain (personal → team → global). Empty name = first match. Resolve(ctx context.Context, userID, connType, name string) (*models.ExtConnection, error) // ResolveAll returns all accessible connections of a type with // decrypted configs. ResolveAll(ctx context.Context, userID, connType string) ([]models.ExtConnection, error) } // BuildConnectionsModule creates the "connections" module. func BuildConnectionsModule(ctx context.Context, resolver ConnectionResolver, userID string) *starlarkstruct.Module { return MakeModule("connections", starlark.StringDict{ "get": starlark.NewBuiltin("connections.get", func( thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, ) (starlark.Value, error) { var connType string var name string if err := starlark.UnpackArgs(b.Name(), args, kwargs, "type", &connType, "name?", &name, ); err != nil { return nil, err } conn, err := resolver.Resolve(ctx, userID, connType, name) if err != nil || conn == nil { return starlark.None, nil } return connectionToDict(conn), nil }), "list": starlark.NewBuiltin("connections.list", func( thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, ) (starlark.Value, error) { var connType string if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &connType); err != nil { return nil, err } conns, err := resolver.ResolveAll(ctx, userID, connType) if err != nil { return starlark.NewList(nil), nil } elems := make([]starlark.Value, len(conns)) for i := range conns { elems[i] = connectionToDict(&conns[i]) } return starlark.NewList(elems), nil }), }) } // connectionToDict converts an ExtConnection to a Starlark dict. // Config fields are flattened into the top-level dict alongside id/name/type. func connectionToDict(conn *models.ExtConnection) *starlark.Dict { d := starlark.NewDict(4 + len(conn.Config)) d.SetKey(starlark.String("id"), starlark.String(conn.ID)) d.SetKey(starlark.String("type"), starlark.String(conn.Type)) d.SetKey(starlark.String("name"), starlark.String(conn.Name)) d.SetKey(starlark.String("scope"), starlark.String(conn.Scope)) for k, v := range conn.Config { switch val := v.(type) { case string: d.SetKey(starlark.String(k), starlark.String(val)) case float64: d.SetKey(starlark.String(k), starlark.Float(val)) case bool: d.SetKey(starlark.String(k), starlark.Bool(val)) default: // Fallback: coerce to string d.SetKey(starlark.String(k), starlark.String("")) } } return d }